// 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' import { getRequestOptions, randomIntFromInterval, sendGetRequest } from '../helpers/libs/utils.mjs' import fs from 'fs' import path from 'path' let reallySendPost = false // reallySendPost = true async function getFollowers (username, instance) { 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` 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' const instance = 'https://mastodon.cipherbliss.com' // TODO get the followers from a csv file if there is one in assets/data/follows.csv const followers = await getFollowers(userIdOnInstance, instance) console.log('followers.length', followers.length) 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) console.log('account', account.acct) avatars_urls.push(account.avatar_static) }) message += '\n #fedifollows #curatorRecommendations' await generateAvatarComposite(avatars_urls) 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/', }) let imagePath = `${path.resolve()}/${compositeFileName}` console.log('------- imagePath', imagePath) if (configPost.reallySendPost) { /** * 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] console.log('\n\n id du média pour le post:', id, configPost) 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' console.log('message', message) console.log('md_message', md_message) // upload de fileName et création du post par le Curator let configPost = { author: 'curator', website: 'cipherbliss', visibility: 'public', language: 'fr', sensitive: false, postObject: {}, folder_image: '.', scheduled_at: '', scheduled_at_bool: false, content_type: 'text/markdown', // image: compositeFileName, message: md_message, reallySendPost } // publishOnMastodon(folderUnpublished, configPost) }, err => { console.error(err) })