//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.date(tweet.created_at, {timeStyle:"short", timeZone:data.config.timezone?.name})} on ${imports.date(tweet.created_at, {dateStyle:"short", timeZone:data.config.timezone?.name})}`
tweet.text = imports.htmlescape( //eslint-disable-line function-paren-newline
//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 ') //eslint-disable-line no-misleading-character-class, prefer-named-capture-group
//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}}
}
}