multi-account-post-schedule.../helpers/libs/utils.mjs

527 lines
13 KiB
JavaScript

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(/<img\s[^>]*?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<unknown>}
*/
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<Object>}
* @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 ''
}
}