//Setup export default async function({login, imports, data, q, account}, {enabled = false, token = ""} = {}) { //Plugin execution try { //Check if plugin is enabled and requirements are met if ((!enabled) || (!q.tweets)) return null //Load inputs let {limit, user: username, attachments} = imports.metadata.plugins.tweets.inputs({data, account, q}) //Load user profile console.debug(`metrics/compute/${login}/plugins > tweets > loading twitter profile (@${username})`) const {data: {data: profile = null}} = await imports.axios.get(`https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url,verified`, {headers: {Authorization: `Bearer ${token}`}}) //Load profile image if (profile?.profile_image_url) { console.debug(`metrics/compute/${login}/plugins > tweets > loading profile image`) profile.profile_image = await imports.imgb64(profile.profile_image_url) } //Load tweets console.debug(`metrics/compute/${login}/plugins > tweets > querying api`) const {data: {data: tweets = [], includes: {media = []} = {}}} = await imports.axios.get( `https://api.twitter.com/2/tweets/search/recent?query=from:${username}&tweet.fields=created_at,entities&media.fields=preview_image_url,url,type&expansions=entities.mentions.username,attachments.media_keys`, {headers: {Authorization: `Bearer ${token}`}}, ) const medias = new Map(media.map(({media_key, type, url, preview_image_url}) => [media_key, (type === "photo") || (type === "animated_gif") ? url : type === "video" ? preview_image_url : null])) //Limit tweets if (limit > 0) { console.debug(`metrics/compute/${login}/plugins > tweets > keeping only ${limit} tweets`) tweets.splice(limit) } //Format tweets await Promise.all(tweets.map(async tweet => { //Mentions and urls tweet.mentions = tweet.entities?.mentions?.map(({username}) => username) ?? [] tweet.urls = new Map(tweet.entities?.urls?.map(({url, display_url: link}) => [url, link]) ?? []) //Attachments if (attachments) { //Retrieve linked content let linked = null if (tweet.urls.size) { linked = [...tweet.urls.keys()][tweet.urls.size - 1] tweet.text = tweet.text.replace(new RegExp(`(?:${linked})$`), "") } //Medias if (tweet.attachments) tweet.attachments = await Promise.all(tweet.attachments.media_keys.filter(key => medias.get(key)).map(key => medias.get(key)).map(async url => ({image: await imports.imgb64(url, {height: -1, width: 450})}))) if (linked) { const {result: {ogImage, ogSiteName: website, ogTitle: title, ogDescription: description}} = await imports.opengraph({url: linked}) const image = await imports.imgb64(ogImage?.url, {height: -1, width: 450, fallback: false}) if (image) { if (tweet.attachments) tweet.attachments.unshift([{image, title, description, website}]) else tweet.attachments = [{image, title, description, website}] } else { tweet.text = `${tweet.text}\n${linked}` } } } else { tweet.attachments = null } //Format text console.debug(`metrics/compute/${login}/plugins > tweets > formatting tweet ${tweet.id}`) tweet.createdAt = `${imports.format.date(tweet.created_at, {time: true})} on ${imports.format.date(tweet.created_at, {date: true})}` tweet.text = imports.htmlescape( //Escape tags imports.htmlescape(tweet.text, {"<": true, ">": true}) //Mentions .replace(new RegExp(`@(${tweet.mentions.join("|")})`, "gi"), '@$1') //Hashtags (this regex comes from the twitter source code) .replace( /(?#$1 ', ) //Line breaks .replace(/\n/g, "
") //Links .replace(new RegExp(`${tweet.urls.size ? "" : "noop^"}(${[...tweet.urls.keys()].map(url => `(?:${url})`).join("|")})`, "gi"), (_, url) => `${tweet.urls.get(url)}`), {"&": true}, ) })) //Result return {username, profile, list: tweets} } //Handle errors catch (error) { let message = "An error occured" if (error.isAxiosError) { const status = error.response?.status const description = error.response?.data?.errors?.[0]?.message ?? null message = `API returned ${status}${description ? ` (${description})` : ""}` error = error.response?.data ?? null } throw {error: {message, instance: error}} } }