diff --git a/source/plugins/tweets/README.md b/source/plugins/tweets/README.md index 2377cdb1..c20734c9 100644 --- a/source/plugins/tweets/README.md +++ b/source/plugins/tweets/README.md @@ -31,6 +31,7 @@ Create an app from your [developer dashboard](https://developer.twitter.com/en/p # ... other options plugin_tweets: yes plugin_tweets_token: ${{ secrets.TWITTER_TOKEN }} # Required + plugin_tweets_attachments: yes # Display tweets attachments (images, preview urls, etc.) plugin_tweets_limit: 2 # Limit to 2 tweets plugin_tweets_user: .user.twitter # Defaults to your GitHub linked twitter username ``` diff --git a/source/plugins/tweets/index.mjs b/source/plugins/tweets/index.mjs index 3e53a1c7..f9337685 100644 --- a/source/plugins/tweets/index.mjs +++ b/source/plugins/tweets/index.mjs @@ -7,7 +7,7 @@ return null //Load inputs - let {limit, user:username} = imports.metadata.plugins.tweets.inputs({data, account, q}) + 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})`) @@ -21,7 +21,8 @@ //Load tweets console.debug(`metrics/compute/${login}/plugins > tweets > querying api`) - const {data:{data:tweets = []}} = await imports.axios.get(`https://api.twitter.com/2/tweets/search/recent?query=from:${username}&tweet.fields=created_at&expansions=entities.mentions.username`, {headers:{Authorization:`Bearer ${token}`}}) + 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) { @@ -31,8 +32,29 @@ //Format tweets await Promise.all(tweets.map(async tweet => { - //Mentions - tweet.mentions = tweet.entities?.mentions.map(({username}) => username) ?? [] + //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})}))) + else 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) + tweet.attachments = [{image, title, description, website}] + } + } + 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})}` @@ -40,13 +62,13 @@ //Escape tags imports.htmlescape(tweet.text, {"<":true, ">":true}) //Mentions - .replace(new RegExp(`@(${tweet.mentions.join("|")})`, "gi"), ' @$1 ') + .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(/https?:[/][/](?t.co[/]\w+)/g, ' $ '), {"&":true}) + .replace(new RegExp(`${tweet.urls.size ? "" : "noop^"}(${[...tweet.urls.keys()].map(url => `(?:${url})`).join("|")})`, "gi"), (_, url) => `${tweet.urls.get(url)}`), {"&":true}) })) //Result diff --git a/source/plugins/tweets/metadata.yml b/source/plugins/tweets/metadata.yml index 3e207935..efd9b2d6 100644 --- a/source/plugins/tweets/metadata.yml +++ b/source/plugins/tweets/metadata.yml @@ -19,6 +19,12 @@ inputs: type: token default: "" + # Display tweets attachments (images, video previews, etc.) + plugin_tweets_attachments: + description: Display tweets attchments + type: boolean + default: no + # Number of tweets to display plugin_tweets_limit: description: Maximum number of tweets to display diff --git a/source/templates/classic/partials/tweets.ejs b/source/templates/classic/partials/tweets.ejs index 238e6189..db809676 100644 --- a/source/templates/classic/partials/tweets.ejs +++ b/source/templates/classic/partials/tweets.ejs @@ -25,9 +25,23 @@ <% if (plugins.tweets.profile) { %> <% if (plugins.tweets.list.length) { %> - <% for (const {text, createdAt } of plugins.tweets.list) { %> + <% for (const {text, createdAt, attachments} of plugins.tweets.list) { %>
<%- text %> + <% if (attachments) { %> +
+ <% for (const {image, title, description, website} of attachments) { %> +
+ <% if (title) { %> +
+
<%= title %>
+
<%= description %>
+
+ <% } %> +
+ <% } %> +
+ <% } %>
<%= createdAt %>
<% } %> diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index 1e422e99..7c7c8d4a 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -326,7 +326,6 @@ .tweet .mention, .tweet .link, .tweet .hashtag { color: #0366d6; - margin: 0 4px; } .tweet .date { @@ -335,6 +334,50 @@ color: #666666; } + .tweet .attachments { + display: flex; + width: 450px; + margin-top: 8px; + } + + .tweet .attachments > div { + flex: 1 1 0; + width: 0; + border-radius: 6px; + background-position: center; + background-size: cover; + height: 200px; + margin: 2px; + box-shadow: 0px 0px 1px #777777A0; + overflow: hidden; + display: flex; + align-items: flex-end; + } + + .tweet .attachments .infos { + background-color: #000000D0; + color: white; + display: flex; + flex-direction: column; + width: 100%; + padding-bottom: 4px; + } + + .tweet .attachments .infos > div { + margin: 4px 8px 0; + } + + .tweet .attachments .infos .title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .tweet .attachments .infos .description { + font-size: 11px; + color: #666666; + } + /* Charts and graphs */ .chart { padding: 0 8px;