import Masto from 'mastodon' import dotenv from 'dotenv' import sharp from 'sharp' import fs from 'fs' import https from 'https' import moment from 'moment' import Parser from 'rss-parser' import { load } from 'cheerio' let local_node_env_conf = dotenv.config() const myArgs = process.argv.slice(2) export const reallySendPost = hasCliArgument('--force') export const folderBlogPostsPreview = process.cwd() + '/assets/blog_posts_medias/' export function randomIntFromInterval (min, max) { // min and max included return Math.floor(Math.random() * (max - min + 1) + min) | 1 } export function getRandomElementOfArray (listItems) { return listItems[Math.floor(Math.random() * listItems.length)] } let nowDate = new Date() export let defaultConfigMasto = { author: 'curator', visibility: 'public', language: 'fr', sensitive: false, disable_slugify: false, reallySendPost, image: '', folder_image: process.cwd() + '/assets/blog_posts_medias/', message: 'Hey coucou! on est le' + nowDate, scheduled_at: '', scheduled_at_bool: false, content_type: 'text/markdown', website: 'qzine', slug: 'default_post_title', postObject: {}, } export function tokenForAuthorIsPresentInDotEnv (author) { return process.env['TOKEN_' + author.toUpperCase()] } /** * create a masto fetcher instance * @param userNickName * @returns {Mastodon} */ export function createMastoFetcherWithAuthorLogin (userNickName) { let accessToken = process.env['TOKEN_' + userNickName.toUpperCase()] const masto = new Masto({ access_token: accessToken, api_url: process.env.INSTANCE_MASTODON + '/api/v1/', }) return masto } /** * send post to mastodon with config * @param config * @returns {*} */ export default function sendPostMastodon (config) { // console.log('send post', config.postObject.post_guid , config.postObject.guid ) // override defaults with input argument config = { ...defaultConfigMasto, ...config, } // console.log("sendPostMastodon config", config) if (!config.reallySendPost) { console.log('\n\n =========== le message ne sera PAS réellement posté sur le compte @' + config.author + '@' + process.env.INSTANCE_MASTODON + ' =========== \n') // console.log('configPost.folder_image', config.folder_image) console.log('config', config.message) } else { console.log(' ') if (process.env.INSTANCE_MASTODON && tokenForAuthorIsPresentInDotEnv(config.author)) { let visibility = 'public' let language = 'fr' let sensitive = false let accessToken = process.env['TOKEN_' + config.author.toUpperCase()] const masto = new Masto({ access_token: accessToken, api_url: process.env.INSTANCE_MASTODON + '/api/v1/', }) let params = { status: config.message, visibility, language, sensitive } if (config.cw) { params['spoiler_text'] = config.cw } if (config.scheduled_at && config.scheduled_at_bool) { let dateschedule = new Date(config.scheduled_at) params['scheduled_at'] = dateschedule.toISOString() } /** * envoi sans fichier joint */ if (!config.image) { console.log('pas d image dans la config') if (config.reallySendPost) { masto.post('statuses', params).then(rep => { console.log('posté, yay!') }, err => { console.error(err) }) } } /** * envoi avec fichier, * on doit d'abord faire un upload du fichier, * puis relier son id de média au nouveau post. */ else if (config.image) { var id console.log('envoi du média', config.image) // upload new media return masto.post('media', { file: fs.createReadStream(config.image) }) .then(resp => { id = resp.data.id params.media_ids = [id] console.log('\n ✅ image, id', id) masto.post('statuses', params).then(rep => { // console.log('rep', rep) console.log('\n ✅ posté avec une nouvelle image, WOOT') }, err => { console.error(err) console.log('erreur T_T') }) }) } // } } else { console.error(`pas de token pour l'auteur "${config.author}" ou pas d'instance mastodon définie`) } } } // Slugify a string export function slugify (str) { str = str.replace(/^\s+|\s+$/g, '') // Make the string lowercase str = str.toLowerCase() // Remove accents, swap ñ for n, etc var from = 'ÁÄÂÀÃÅČÇĆĎÉĚËÈÊẼĔȆÍÌÎÏŇÑÓÖÒÔÕØŘŔŠŤÚŮÜÙÛÝŸŽáäâàãåčçćďéěëèêẽĕȇíìîïňñóöòôõøðřŕšťúůüùûýÿžþÞĐđßÆa·/_,:;' var to = 'AAAAAACCCDEEEEEEEEIIIINNOOOOOORRSTUUUUUYYZaaaaaacccdeeeeeeeeiiiinnooooooorrstuuuuuyyzbBDdBAa------' for (var i = 0, l = from.length; i < l; i++) { str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i)) } // Remove invalid chars str = str.replace(/[^a-z0-9 -]/g, '') // Collapse whitespace and replace by - .replace(/\s+/g, '-') // Collapse dashes .replace(/-+/g, '-') return str } /** * @name listFilesOfFolder * lister les noms de fichier que l'on peut publier dans un dossier. * retourne un tableau */ export function listFilesOfFolder (folderPath) { let filesNames = [] fs.readdirSync(folderPath).map(fileName => { return filesNames.push(fileName) }) return filesNames } /** * @name initializeFolderForPictures * crée un dossier d'assets, avec ses sous dossiers not_published et published si ils manquent. * une fois que l'on prendra une image dans le dossier non publié, on la déplacera dans le dossier des images publées. */ export function initializeFolderForPictures (folderName) { try { if (!fs.existsSync(folderName)) { fs.mkdirSync(folderName) } } catch (err) { console.error(err) } } /** * find first image in blog post and return the src value of the img tag * @param htmlContent * @returns {string} */ export function findFirstImageInContent (htmlContent = '') { let result = '' let foundPictures = htmlContent.match(/]*?src\s*=\s*['\"]([^'\"]*?)['\"][^>]*?>/) let first = '' if (foundPictures && foundPictures[0]) { first = foundPictures[0] } else { console.log('pas d image trouvée dans le contenu ', htmlContent) } if (first) { result = first.match(/src\=\"(.*)\"/i) if (result.length && result[0]) { result = result[0].split('"') result = result[1] } } result = clearLink(result) console.log('clearLink', result) return result } function clearLink (linkString) { linkString = linkString.replace('http:', 'https:') linkString = linkString.replace('https://www.ailesse.info/~tykayn/bazar/kotlife', 'https://www.tykayn.fr/wp-content/uploads/i/kotlife') linkString = linkString.replace('https://blog.artlemoine.com/public/i', 'https://www.tykayn.fr/wp-content/uploads/i') linkString = linkString.replace('https://www.ailesse.com/%7Etykayn/bazar', 'https://www.tykayn.fr/wp-content/uploads/i/bazar') return linkString } /** * usage: * downloadImage('https://upload.wikimedia.org/wikipedia/en/thumb/7/7d/Lenna_%28test_image%29.png/440px-Lenna_%28test_image%29.png', 'lena.png') * .then(console.log) * .catch(console.error); * @param url * @param filepath * @returns {Promise} */ export function downloadImage (url, filepath) { return new Promise((resolve, reject) => { const options = { headers: { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36 Edg/107.0.1418.52' } } https.get(url, options, (res) => { if (res.statusCode === 200) { res.pipe(fs.createWriteStream(filepath)) .on('error', reject) .once('close', () => resolve(filepath)) } else { // Consume response data to free up memory res.resume() reject(new Error(`Request Failed With a Status Code: ${res.statusCode}; \n ${res.statusMessage} `)) } }) }) } /** * faire un * @param pictureName * @param width * @param height * @returns {Promise} * @constructor */ export function CropPicture (pictureName, width = 500, height = 300) { return sharp(pictureName) .extract({ left: 0, top: 0, width, height }) .toFile('thumb_' + pictureName, function (err) { if (err) console.log(err) }) } /** * prendre un post parmi tous ceux du blog, dans ceux qui ont été publiés * @returns {*} */ export function getRandomLinkGeneral (tkpostsjson) { let filteredLinks = [] if (tkpostsjson[0].post_status) { filteredLinks = tkpostsjson.filter(elem => elem.post_status === 'publish') } else if (tkpostsjson[0].status) { filteredLinks = tkpostsjson.filter(elem => elem.status === 'publish') } return getRandomElementOfArray(filteredLinks) } /** * trouver l'image du contenu si il y en a * @param postContent * @param configPost */ export function findPictureAndSendPost (postContent, configPost) { let firstPictureSource = findFirstImageInContent(postContent) let filePathForDownloadedImage = `${configPost.folder_image}_${configPost.website}_media_post_${slugify(configPost.slug)}.jpg` if (firstPictureSource) { console.log('firstPictureSource found', firstPictureSource) // check if picture already exist console.log('on envoie le média et l image : ', filePathForDownloadedImage) downloadImage(firstPictureSource, filePathForDownloadedImage) .then((res) => { // suite du poste avec upload d'image console.log('média téléchargé, on envoie le post') configPost.image = filePathForDownloadedImage sendPostMastodon(configPost) }, (err) => { console.log('pas dimage trouvée pour l URL ', firstPictureSource, err) sendPostMastodon(configPost) } ) .catch((err) => { console.log('erreur avec cette URL ', firstPictureSource, err) sendPostMastodon(configPost) }) } else { // no image provided console.log('pas d\'image dans le corps du texte', configPost.image) // on envoie avec l'image par défaut sendPostMastodon(configPost) } } /** * find cli argument * @param argument * @returns {boolean} */ export function hasCliArgument (argument) { return myArgs.indexOf(argument) !== -1 } let parser = new Parser() export function diffDaysBetweenTwoDates (date1, date2) { const a = moment(date1) const b = moment(date2) return a.diff(b, 'days') } export function filterRegionAgendaDuLibreEvents (events_list, filter_critera) { let selection = [] events_list.forEach(item => { if (item.region_id == filter_critera) { selection.push(item) } }) return selection } moment.locale('fr') export function groupEventsByDay (events_list) { let selection = {} events_list.forEach(item => { let formattedDay = moment(item.start_time).format('dddd DD') if (!selection[formattedDay]) { selection[formattedDay] = [] } selection[formattedDay].push(item) }) return selection } export function convertHTMLtoMD (htmlContent) { const $ = load(htmlContent) translateNode($) return $.html() } export function translateNode ($) { const elementsToTranslate = [ { tag: 'h1', mdTag: '##' }, { tag: 'h2', mdTag: '###' }, { tag: 'h3', mdTag: '####' }, { tag: 'h4', mdTag: '#####' }, { tag: 'h5', mdTag: '######' }, { tag: 'p', mdTag: '\n\n' }, { tag: 'ul', preserveChildren: true, transformChild: ($child) => '\n- ' + $.text($child) }, { tag: 'ol', preserveChildren: true, transformChild: ($child, idx) => `\n${idx + 1}. ${$.text($child)}` }, { tag: 'a', parseAttr: ('href', link => `[${link}](${link})`) }, { tag: 'strong', mdFormat: '$&' }, { tag: 'em', mdFormat: '_$&_' }, { tag: 'code', mdFormat: '`$&`' }, ] elementsToTranslate.forEach(element => { $(element.tag).each((_, el) => { const $el = $(el) if (element.parseAttr) { const attrVal = $el.attr(element.parseAttr[0]) if (attrVal) { $el.replaceWith(element.parseAttr[1](attrVal)) } else { $el.remove() } } else if (element.transformChild) { $el.contents().filter(() => !!this.parent()).each((_, childEl) => { const transformedText = element.transformChild($(childEl), $el.index()) $el.before('\n' + transformedText) }) $el.remove() } else { $el.replaceWith(element.mdTag + $el.text() + element.mdFormat) } }) }) } export function getRequestOptions (host, port, path) { const options = { host: host, port: port, path: path, method: 'GET', headers: { Accept: 'application/activity+json', 'User-Agent': 'MyApp/1.0.0 (contact@myemail.org)', }, } return options } export async function sendGetRequest (options) { return new Promise((resolve, reject) => { const req = http.request(options, (res) => { let body = Buffer.alloc(0) res.on('data', (chunk) => { body = Buffer.concat([body, chunk]) }) res.on('end', () => { resolve(JSON.parse(body.toString())) }) }) req.on('error', (err) => { reject(err) }) req.end() }) } const splitTextIntoChunks = (text, limit) => { const words = text.trim().split(/\s+/g) const chunks = [] let currentChunk = [] let wordCount = 0 for (const word of words) { if (wordCount + word.length > limit) { chunks.push(currentChunk.join(' ')) currentChunk = [word] wordCount = word.length } else { currentChunk.push(word) wordCount += word.length + 1 } } if (currentChunk.length > 0) { const joinedWords = currentChunk.join(' ') chunks.push(joinedWords.length <= limit ? joinedWords : joinedWords.substr(0, limit) + '...') } return chunks } export function splitLongDescription (text, limit) { let chunks = splitTextIntoChunks(text, limit) if (chunks) { if (chunks[0]) { return chunks[0] } return '' } }