Add tweets plugin (#23)

* Add support for tweets plugin
This commit is contained in:
Simon Lecoq
2020-12-17 22:38:51 +01:00
committed by GitHub
parent 6eac410f03
commit 592fd9d7bb
21 changed files with 312 additions and 316 deletions

View File

@@ -36,6 +36,7 @@
gists:"Gists metrics",
topics:"Starred topics",
projects:"Projects",
tweets:"Latest tweets",
"base.header":"Header",
"base.activity":"Account activity",
"base.community":"Community stats",
@@ -55,6 +56,7 @@
"projects.limit":4,
"topics.sort":"stars",
"topics.limit":12,
"tweets.limit":2,
},
},
templates:{

View File

@@ -51,9 +51,16 @@
</label>
</div>
<i>*Additional plugins may be available when used as GitHub Action</i>
<template v-if="(plugins.enabled.music)||(plugins.enabled.pagespeed)||(plugins.enabled.languages)||(plugins.enabled.habits)||(plugins.enabled.posts)||(plugins.enabled.isocalendar)||(plugins.enabled.projects)||(plugins.enabled.topics)">
<template v-if="(plugins.enabled.tweets)||(plugins.enabled.music)||(plugins.enabled.pagespeed)||(plugins.enabled.languages)||(plugins.enabled.habits)||(plugins.enabled.posts)||(plugins.enabled.isocalendar)||(plugins.enabled.projects)||(plugins.enabled.topics)">
<h3>2.3 Configure additional plugins</h3>
<div class="options">
<div class="options-group" v-if="plugins.enabled.tweets">
<h4>{{ plugins.descriptions.tweets }}</h4>
<label>
Number of tweets to display
<input type="number" v-model="plugins.options['tweets.limit']" min="1" max="10" @change="load">
</label>
</div>
<div class="options-group" v-if="plugins.enabled.music">
<h4>{{ plugins.descriptions.music }}</h4>
<label>

View File

@@ -49,7 +49,7 @@
//Compute metrics
console.debug(`metrics/compute/${login} > compute`)
const computer = Templates[template].default || Templates[template]
await computer({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, format, bytes, shuffle, htmlescape}})
await computer({login, q}, {conf, data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, format, bytes, shuffle, htmlescape, urlexpand}})
const promised = await Promise.all(pending)
//Check plugins errors
@@ -115,13 +115,22 @@
}
/** Escape html */
function htmlescape(string) {
function htmlescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) {
return string
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;")
.replace(/&(?!(?:amp|lt|gt|quot|apos);)/g, u["&"] ? "&amp;" : "&")
.replace(/</g, u["<"] ? "&lt;" : "<")
.replace(/>/g, u[">"] ? "&gt;" : ">")
.replace(/"/g, u['"'] ? "&quot;" : '"')
.replace(/'/g, u["'"] ? "&apos;" : "'")
}
/** Expand url */
async function urlexpand(url) {
try {
return (await axios.get(url)).request.res.responseUrl
} catch {
return url
}
}
/** Placeholder generator */
@@ -169,6 +178,7 @@
languages:{favorites:new Array(7).fill(null).map((_, x) => ({x, name:"######", color:"#ebedf0", value:1/(x+1)}))},
topics:{list:[...new Array("topics.limit" in q ? Math.max(Number(q["topics.limit"])||0, 0) : 12).fill(null).map(() => ({name:"######", description:"", icon:null})), {name:`And ## more...`, description:"", icon:null}]},
projects:{list:[...new Array("projects.limit" in q ? Math.max(Number(q["projects.limit"])||0, 0) : 4).fill(null).map(() => ({name:"########", updated:"########", progress:{enabled:true, todo:"##", doing:"##", done:"##", total:"##"}}))]},
tweets:{profile:{username:"########", verified:false}, list:[...new Array("tweets.limit" in q ? Math.max(Number(q["tweets.limit"])||0, 0) : 2).fill(null).map(() => ({text:"###### ###### ####### ######".repeat(4), created_at:Date.now()}))]},
}[key]??{})]
)),
})

View File

@@ -13,6 +13,7 @@
import projects from "./projects/index.mjs"
import topics from "./topics/index.mjs"
import traffic from "./traffic/index.mjs"
import tweets from "./tweets/index.mjs"
//Exports
export default {
@@ -28,4 +29,5 @@
projects,
topics,
traffic,
tweets,
}

View File

@@ -37,6 +37,9 @@
break
topics.push(...starred)
}
//Close browser
console.debug(`metrics/compute/${login}/plugins > music > closing browser`)
await browser.close()
//Shuffle topics
if (shuffle)
topics = imports.shuffle(topics)

View File

@@ -0,0 +1,56 @@
//Setup
export default async function ({login, imports, data, q}, {enabled = false, token = null} = {}) {
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
if ((!enabled)||(!q.tweets))
return null
//Parameters override
let {"tweets.limit":limit = 2} = q
//Limit
limit = Math.max(1, Math.min(10, Number(limit)))
//Load user profile
const username = data.user.twitterUsername
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 tweets
console.debug(`metrics/compute/${login}/plugins > tweets > loading tweets`)
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}`}})
//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)
}
//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
tweet.mentions = tweet.entities?.mentions.map(({username}) => username) ?? []
//Format text
console.debug(`metrics/compute/${login}/plugins > tweets > formatting tweet ${tweet.id}`)
tweet.text = imports.htmlescape(
//Escape tags
imports.htmlescape(tweet.text, {"<":true, ">":true})
//Mentions
.replace(new RegExp(`@(${tweet.mentions.join("|")})`, "gi"), ` <span class="mention">@$1</span> `)
//Hashtags
.replace(/(?<!&)[#|]([a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f]*[a-z_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f][a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f]*)/gi, ` <span class="hashtag">#$1</span> `)
//Line breaks
.replace(/\n/g, "<br/>")
//Links
.replace(/https?:[/][/](t.co[/]\w+)/g, ` <span class="link">$1</span> `)
, {"&":true})
}))
//Result
return {username, profile, list:tweets}
}
//Handle errors
catch (error) {
console.debug(error)
throw {error:{message:`An error occured`}}
}
}

View File

@@ -14,6 +14,7 @@
+ (!!plugins.gists)*68
+ (!!plugins.topics)*160
+ (!!plugins.projects)*22 + (plugins.projects?.list?.length ?? 0)*60
+ (!!plugins.tweets)*64 + (plugins.tweets?.list?.length ?? 0)*90
+ Math.max(0, (((!!base.metadata)+(!!base.header)+((!!base.activity)||(!!base.community))+(!!base.repositories)+((!!plugins.habits))+(!!plugins.pagespeed)+(!!plugins.languages)+(!!plugins.music)+(!!plugins.posts)+(!!plugins.isocalendar)+(!!plugins.gists)+(!!plugins.topics)+(!!plugins.projects))-1))*4
%>">
@@ -633,6 +634,52 @@
</section>
<% } %>
<% if (plugins.tweets) { %>
<section>
<h2 class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 273.5 222.3" width="16" height="16"><path d="M273.5 26.3a109.77 109.77 0 0 1-32.2 8.8 56.07 56.07 0 0 0 24.7-31 113.39 113.39 0 0 1-35.7 13.6 56.1 56.1 0 0 0-97 38.4 54 54 0 0 0 1.5 12.8A159.68 159.68 0 0 1 19.1 10.3a56.12 56.12 0 0 0 17.4 74.9 56.06 56.06 0 0 1-25.4-7v.7a56.11 56.11 0 0 0 45 55 55.65 55.65 0 0 1-14.8 2 62.39 62.39 0 0 1-10.6-1 56.24 56.24 0 0 0 52.4 39 112.87 112.87 0 0 1-69.7 24 119 119 0 0 1-13.4-.8 158.83 158.83 0 0 0 86 25.2c103.2 0 159.6-85.5 159.6-159.6 0-2.4-.1-4.9-.2-7.3a114.25 114.25 0 0 0 28.1-29.1"></path></svg>
Latest tweets
</h2>
<div class="row fill-width">
<section>
<% if (plugins.tweets.error) { %>
<div class="field error">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
<%= plugins.tweets.error.message %>
</div>
<% } else { %>
<div class="field <%= !plugins.tweets.profile ? 'error' : '' %>">
<% if (plugins.tweets.profile?.verified) { %>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M9.585.52a2.678 2.678 0 00-3.17 0l-.928.68a1.178 1.178 0 01-.518.215L3.83 1.59a2.678 2.678 0 00-2.24 2.24l-.175 1.14a1.178 1.178 0 01-.215.518l-.68.928a2.678 2.678 0 000 3.17l.68.928c.113.153.186.33.215.518l.175 1.138a2.678 2.678 0 002.24 2.24l1.138.175c.187.029.365.102.518.215l.928.68a2.678 2.678 0 003.17 0l.928-.68a1.17 1.17 0 01.518-.215l1.138-.175a2.678 2.678 0 002.241-2.241l.175-1.138c.029-.187.102-.365.215-.518l.68-.928a2.678 2.678 0 000-3.17l-.68-.928a1.179 1.179 0 01-.215-.518L14.41 3.83a2.678 2.678 0 00-2.24-2.24l-1.138-.175a1.179 1.179 0 01-.518-.215L9.585.52zM7.303 1.728c.415-.305.98-.305 1.394 0l.928.68c.348.256.752.423 1.18.489l1.136.174c.51.078.909.478.987.987l.174 1.137c.066.427.233.831.489 1.18l.68.927c.305.415.305.98 0 1.394l-.68.928a2.678 2.678 0 00-.489 1.18l-.174 1.136a1.178 1.178 0 01-.987.987l-1.137.174a2.678 2.678 0 00-1.18.489l-.927.68c-.415.305-.98.305-1.394 0l-.928-.68a2.678 2.678 0 00-1.18-.489l-1.136-.174a1.178 1.178 0 01-.987-.987l-.174-1.137a2.678 2.678 0 00-.489-1.18l-.68-.927a1.178 1.178 0 010-1.394l.68-.928c.256-.348.423-.752.489-1.18l.174-1.136c.078-.51.478-.909.987-.987l1.137-.174a2.678 2.678 0 001.18-.489l.927-.68zM11.28 6.78a.75.75 0 00-1.06-1.06L7 8.94 5.78 7.72a.75.75 0 00-1.06 1.06l1.75 1.75a.75.75 0 001.06 0l3.75-3.75z"></path></svg>
<% } else { %>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M4.75 2.37a6.5 6.5 0 006.5 11.26.75.75 0 01.75 1.298 8 8 0 113.994-7.273.754.754 0 01.006.095v1.5a2.75 2.75 0 01-5.072 1.475A4 4 0 1112 8v1.25a1.25 1.25 0 002.5 0V7.867a6.5 6.5 0 00-9.75-5.496V2.37zM10.5 8a2.5 2.5 0 10-5 0 2.5 2.5 0 005 0z"></path></svg>
<% } %>
<%= plugins.tweets.username %>
<% if (!plugins.tweets.profile) { %>
: twitter username not found
<% } %>
</div>
<% if (plugins.tweets.profile) { %>
<% if (plugins.tweets.list.length) { %>
<% for (const {text, created_at} of plugins.tweets.list) { %>
<div class="tweet">
<%- text %>
<div class="date"><%= new Date(created_at).toGMTString() %></div>
</div>
<% } %>
<% } else { %>
<div class="field">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M1.75 1.5a.25.25 0 00-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 01.75.75v2.19l2.72-2.72a.75.75 0 01.53-.22h6.5a.25.25 0 00.25-.25v-9.5a.25.25 0 00-.25-.25H1.75zM0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0114.25 13H8.06l-2.573 2.573A1.457 1.457 0 013 14.543V13H1.75A1.75 1.75 0 010 11.25v-9.5zM9 9a1 1 0 11-2 0 1 1 0 012 0zm-.25-5.25a.75.75 0 00-1.5 0v2.5a.75.75 0 001.5 0v-2.5z"></path></svg>
No recent tweets
</div>
<% } %>
<% } %>
<% } %>
</section>
</div>
</section>
<% } %>
<% if (plugins.isocalendar) { %>
<section>
<h2 class="field">

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

@@ -7,6 +7,7 @@ query Metrics {
avatarUrl
websiteUrl
isHireable
twitterUsername
gists {
totalCount
}

View File

@@ -284,6 +284,26 @@
flex-wrap: wrap;
}
/* Tweets */
.tweet {
font-size: 13px;
margin-top: 6px;
margin-bottom: 16px;
margin-left: 18px;
border-left: 3px solid #777777B2;
padding-left: 6px;
}
.tweet .mention, .tweet .link, .tweet .hashtag {
color: #0366d6;
}
.tweet .date {
margin: 6px 0;
font-size: 12px;
opacity: .7;
}
/* Fade animation */
.af {
opacity: 0;

View File

@@ -39,7 +39,7 @@
//Compute licenses stats
computed.licenses.favorite = Object.entries(computed.licenses.used).sort(([an, a], [bn, b]) => b - a).slice(0, 1).map(([name, value]) => name) || ""
//Compute total commits and sponsorships
//Compute total commits
computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
//Compute registration date