2024-02-07 17:06:53 +01:00
|
|
|
|
// selectionner un compte parmi les gens suivis dans une liste
|
|
|
|
|
|
|
|
|
|
// export des abonnements utilisateur https://mastodon.cipherbliss.com/settings/exports/follows.csv/home/cipherbliss/Nextcloud/inbox/following_accounts.csv
|
|
|
|
|
|
|
|
|
|
import axios from 'axios'
|
|
|
|
|
import Masto from 'mastodon'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* picture generation
|
|
|
|
|
*/
|
|
|
|
|
import Jimp from 'jimp'
|
2024-02-07 18:25:49 +01:00
|
|
|
|
import { getRequestOptions, randomIntFromInterval, sendGetRequest } from '../helpers/libs/utils.mjs'
|
2024-02-07 17:06:53 +01:00
|
|
|
|
import fs from 'fs'
|
|
|
|
|
import path from 'path'
|
|
|
|
|
|
|
|
|
|
let reallySendPost = false
|
2024-02-07 18:25:49 +01:00
|
|
|
|
|
2024-09-03 10:32:31 +02:00
|
|
|
|
// reallySendPost = true
|
2024-02-07 17:06:53 +01:00
|
|
|
|
|
|
|
|
|
async function getFollowers (username, instance) {
|
2024-02-07 18:25:49 +01:00
|
|
|
|
const rand = randomIntFromInterval(1, 30) * 1000
|
|
|
|
|
console.log('random max id x 1000', rand)
|
|
|
|
|
const url = `${instance}/api/v1/accounts/${username}/following?max_id=${rand}000`
|
2024-02-07 17:06:53 +01:00
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await axios.get(url)
|
|
|
|
|
|
|
|
|
|
return response.data
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error(error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Function to look up the user account number using webfinger
|
|
|
|
|
async function getUserAccountNumberFromMastodonUsername (username) {
|
|
|
|
|
const parsedUsername = username.split('@')
|
|
|
|
|
if (parsedUsername.length !== 2) {
|
|
|
|
|
throw new Error('Invalid Mastodon username format.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const localPart = parsedUsername[0]
|
|
|
|
|
const domain = parsedUsername[1]
|
|
|
|
|
console.log('localPart, domain', localPart, domain)
|
|
|
|
|
|
|
|
|
|
const acctPath = '/.well-known/webfinger?resource=' + encodeURIComponent('acct:' + localPart + '@' + domain)
|
|
|
|
|
const options = getRequestOptions(domain, 443, acctPath)
|
|
|
|
|
|
|
|
|
|
console.log('options', options)
|
|
|
|
|
try {
|
|
|
|
|
const fingerResult = await sendGetRequest(options)
|
|
|
|
|
const actorResource = fingerResult['subject'] || fingerResult.links?.find((item) => item.rel === 'self').href
|
|
|
|
|
|
|
|
|
|
if (!actorResource) {
|
|
|
|
|
throw new Error('Failed to obtain the resource identifier.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const accountHost = actorResource.split('/')[2]
|
|
|
|
|
const accountPath = actorResource.split('/').pop()
|
|
|
|
|
|
|
|
|
|
const accountInfoReqOptions = getRequestOptions(accountHost, 443, '/api/v1/accounts/' + accountPath)
|
|
|
|
|
const accountInfo = await sendGetRequest(accountInfoReqOptions)
|
|
|
|
|
|
|
|
|
|
return accountInfo.id
|
|
|
|
|
} catch (error) {
|
|
|
|
|
throw error
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function main () {
|
|
|
|
|
|
|
|
|
|
const userIdOnInstance = '1'
|
|
|
|
|
|
2024-02-07 18:25:49 +01:00
|
|
|
|
const instance = 'https://mastodon.cipherbliss.com'
|
2024-02-07 17:06:53 +01:00
|
|
|
|
|
2024-02-07 18:25:49 +01:00
|
|
|
|
// TODO get the followers from a csv file if there is one in assets/data/follows.csv
|
2024-02-07 17:06:53 +01:00
|
|
|
|
const followers = await getFollowers(userIdOnInstance, instance)
|
|
|
|
|
|
|
|
|
|
console.log('followers.length', followers.length)
|
2024-02-07 18:25:49 +01:00
|
|
|
|
|
2024-02-07 17:06:53 +01:00
|
|
|
|
const randomFollowers = followers?.sort(() => Math.random() - 0.5)?.slice(0, 3)
|
|
|
|
|
|
|
|
|
|
// console.log('Random followers:', randomFollowers)
|
|
|
|
|
let message = '\n Les personnes que l\'on vous recommande de suivre aujourd\'hui:'
|
|
|
|
|
|
|
|
|
|
let avatars_urls = []
|
|
|
|
|
randomFollowers.forEach(account => {
|
|
|
|
|
message += '\n' + displayDataAboutFollower(account)
|
|
|
|
|
|
2024-02-07 18:25:49 +01:00
|
|
|
|
console.log('account', account.acct)
|
|
|
|
|
|
2024-02-07 17:06:53 +01:00
|
|
|
|
avatars_urls.push(account.avatar_static)
|
|
|
|
|
})
|
|
|
|
|
message += '\n #fedifollows #curatorRecommendations'
|
|
|
|
|
|
2024-02-07 18:25:49 +01:00
|
|
|
|
await generateAvatarComposite(avatars_urls)
|
2024-02-07 17:06:53 +01:00
|
|
|
|
return message
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* displays username, acct, avatar and url for one account
|
|
|
|
|
* @param follower
|
|
|
|
|
*/
|
|
|
|
|
function displayDataAboutFollower (follower) {
|
|
|
|
|
|
|
|
|
|
let text = ''
|
|
|
|
|
if (follower.note) {
|
|
|
|
|
text = follower.note.trim().substring(0, 150)
|
|
|
|
|
if (follower.note.trim() > 150) {
|
|
|
|
|
text += '...'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ` * _${follower.display_name}_: [${follower.acct}](${follower.url})
|
|
|
|
|
${text.trim()}`
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let folderUnpublished = ''
|
|
|
|
|
let compositeFileName = ''
|
|
|
|
|
|
|
|
|
|
// Function to maintain aspect ratio while scaling an image down.
|
|
|
|
|
function scaleDownPreserveAspectRatio (image, newWidth, newHeight) {
|
|
|
|
|
const originalWidth = image.bitmap.width
|
|
|
|
|
const originalHeight = image.bitmap.height
|
|
|
|
|
|
|
|
|
|
const scaleFactor = Math.min(newWidth / originalWidth, newHeight / originalHeight)
|
|
|
|
|
|
|
|
|
|
const newWidthScaled = Math.round(originalWidth * scaleFactor)
|
|
|
|
|
const newHeightScaled = Math.round(originalHeight * scaleFactor)
|
|
|
|
|
|
|
|
|
|
return image.scaleToFit(newWidthScaled, newHeightScaled)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prend trois URL d'image d'avatar et génère une image composite JPG avec celles-ci,
|
|
|
|
|
* enregistrée sous le format «YYYY-MM-DDTHH-MM-SS.jpg».
|
|
|
|
|
*/
|
|
|
|
|
async function generateAvatarComposite (avatarUrls) {
|
|
|
|
|
if (!Array.isArray(avatarUrls) || avatarUrls.length !== 3) {
|
|
|
|
|
throw new Error('Veuillez fournir exactement trois URL d’avatar.')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Télécharger chaque avatar
|
|
|
|
|
const avatarImagesPromises = avatarUrls.map((url) => downloadImage(url))
|
|
|
|
|
const avatarImages = await Promise.all(avatarImagesPromises)
|
|
|
|
|
|
|
|
|
|
// Combiner les images horizontalement
|
|
|
|
|
// Resize avatars to a consistent dimension of 400x400 pixels while maintaining aspect ratio
|
|
|
|
|
const resizedAvatarImages = avatarImages.map((image) => scaleDownPreserveAspectRatio(image, 400, 400))
|
|
|
|
|
|
|
|
|
|
// Combine the images horizontally
|
|
|
|
|
const combinedWidth = 3 * resizedAvatarImages[0].bitmap.width
|
|
|
|
|
const combinedHeight = Math.max(...resizedAvatarImages.map((image) => image.bitmap.height))
|
|
|
|
|
const composite = new Jimp(combinedWidth, combinedHeight)
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < resizedAvatarImages.length; i++) {
|
|
|
|
|
const xOffset = i * resizedAvatarImages[0].bitmap.width
|
|
|
|
|
composite.composite(resizedAvatarImages[i], xOffset, 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Enregistrer l'image composite
|
|
|
|
|
compositeFileName = `avatars_reccommendations_${Date.now().toString()}.jpg`
|
|
|
|
|
await composite.writeAsync(compositeFileName)
|
|
|
|
|
console.log(`L'image composite a été enregistrée sous le nom "${compositeFileName}".`)
|
|
|
|
|
return compositeFileName
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Télécharge une image depuis l'URL spécifiée et renvoie une instance Jimp.
|
|
|
|
|
*/
|
|
|
|
|
async function downloadImage (url) {
|
|
|
|
|
return await Jimp.read(url)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function publishOnMastodon (folderUnpublished, configPost) {
|
|
|
|
|
|
|
|
|
|
let accessToken = process.env['TOKEN_' + configPost.author.toUpperCase()]
|
|
|
|
|
console.log('accessToken', accessToken)
|
|
|
|
|
const masto = new Masto({
|
|
|
|
|
access_token: accessToken,
|
|
|
|
|
api_url: process.env.INSTANCE_MASTODON + '/api/v1/',
|
|
|
|
|
})
|
|
|
|
|
|
2024-02-07 18:25:49 +01:00
|
|
|
|
let imagePath = `${path.resolve()}/${compositeFileName}`
|
|
|
|
|
console.log('------- imagePath', imagePath)
|
2024-02-07 17:06:53 +01:00
|
|
|
|
|
2024-02-07 18:25:49 +01:00
|
|
|
|
if (configPost.reallySendPost) {
|
2024-02-07 17:06:53 +01:00
|
|
|
|
/**
|
|
|
|
|
* poster le média, puis faire un toot avec le média lié
|
|
|
|
|
*/
|
|
|
|
|
masto.post('media', { file: fs.createReadStream(imagePath) })
|
|
|
|
|
.then(resp => {
|
|
|
|
|
let id = resp.data.id
|
|
|
|
|
configPost.media_ids = [id]
|
2024-02-07 18:25:49 +01:00
|
|
|
|
console.log('\n\n id du média pour le post:', id, configPost)
|
|
|
|
|
|
2024-02-07 17:06:53 +01:00
|
|
|
|
masto.post('statuses', configPost).then(rep => {
|
|
|
|
|
// console.log('rep', rep)
|
|
|
|
|
console.log(`\n\n posté avec une nouvelle image, ${configPost.image} WOOT`)
|
|
|
|
|
}, err => {
|
|
|
|
|
console.error(err)
|
|
|
|
|
console.log('\n\n erreur T_T')
|
|
|
|
|
})
|
|
|
|
|
}, err => {
|
|
|
|
|
console.error(err)
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
console.log('envoi désactivé avec configPost.reallySendPost')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* run all
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
main().then(message => {
|
|
|
|
|
// let md_message = convertHTMLtoMD(message)
|
|
|
|
|
let md_message = message.trim() + '\n'
|
2024-02-07 18:25:49 +01:00
|
|
|
|
console.log('message', message)
|
2024-09-03 10:32:31 +02:00
|
|
|
|
|
2024-02-07 17:06:53 +01:00
|
|
|
|
console.log('md_message', md_message)
|
|
|
|
|
// upload de fileName et création du post par le Curator
|
|
|
|
|
let configPost = {
|
|
|
|
|
author: 'curator',
|
|
|
|
|
website: 'cipherbliss',
|
2024-02-07 18:25:49 +01:00
|
|
|
|
visibility: 'public',
|
|
|
|
|
language: 'fr',
|
|
|
|
|
sensitive: false,
|
2024-02-07 17:06:53 +01:00
|
|
|
|
postObject: {},
|
|
|
|
|
folder_image: '.',
|
2024-02-07 18:25:49 +01:00
|
|
|
|
scheduled_at: '',
|
|
|
|
|
scheduled_at_bool: false,
|
|
|
|
|
content_type: 'text/markdown',
|
|
|
|
|
// image: compositeFileName,
|
2024-02-07 17:06:53 +01:00
|
|
|
|
message: md_message,
|
|
|
|
|
reallySendPost
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-07 18:25:49 +01:00
|
|
|
|
// publishOnMastodon(folderUnpublished, configPost)
|
2024-02-07 17:06:53 +01:00
|
|
|
|
|
|
|
|
|
}, err => {
|
|
|
|
|
console.error(err)
|
|
|
|
|
})
|