From 7df6c922dcd0872461524ed09a8f17b1384d3e82 Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Sat, 9 Jan 2021 18:57:13 +0100 Subject: [PATCH] Add activity plugin (#41) --- README.md | 189 +++++++++++------ action.yml | 79 ++++++-- settings.example.json | 3 + source/app/action/index.mjs | 8 + source/app/metrics.mjs | 15 +- source/app/mocks.mjs | 316 +++++++++++++++++++++++++++-- source/app/web/instance.mjs | 1 + source/app/web/statics/app.js | 2 + source/app/web/statics/index.html | 9 +- source/plugins/activity/index.mjs | 136 +++++++++++++ source/plugins/habits/index.mjs | 2 +- source/plugins/pagespeed/index.mjs | 4 +- source/templates/classic/image.svg | 146 +++++++++++++ source/templates/classic/style.css | 74 +++++++ tests/metrics.test.js | 14 +- 15 files changed, 886 insertions(+), 112 deletions(-) create mode 100644 source/plugins/activity/index.mjs diff --git a/README.md b/README.md index 7fa73676..1ea9c200 100644 --- a/README.md +++ b/README.md @@ -127,7 +127,7 @@ But there's more with [plugins](https://github.com/lowlighter/metrics/tree/maste 💡 Coding Habits plugin - 🎫 Gists plugin + 📰 Activity plugin @@ -141,8 +141,8 @@ But there's more with [plugins](https://github.com/lowlighter/metrics/tree/maste - - + + @@ -163,16 +163,20 @@ But there's more with [plugins](https://github.com/lowlighter/metrics/tree/maste + 🎫 Gists plugin 🗃️ Header special features - + + + + + - @@ -223,8 +227,6 @@ Try it now at [metrics.lecoq.io](https://metrics.lecoq.io/) with your GitHub use Because some plugins require additional configuration and setup, some of them are not available at [metrics.lecoq.io](https://metrics.lecoq.io/). For a fully-featured experience, consider using this as a [GitHub Action](https://github.com/marketplace/actions/github-metrics-as-svg-image) instead! -[![Metrics](https://status.lecoq.io/status/metrics.lecoq.io-443.svg)](https://github.com/lowlighter/downtime) - # 📜 How to use? ## ⚙️ Using GitHub Action on your profile repository (~5 min setup) @@ -526,66 +528,70 @@ Used template defaults to the `classic` one. 🐤 ✒️ 💡 - 🎫 + 📰 🌟 + 🎫 Classic - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ - ✔️ + ✔️ + ✔️ + ✔️ + ✔️ + ✔️ + ✔️ + ✔️ + ✔️ + ✔️ + ✔️ + ✔️ + ✔️ + ✔️ + ✔️M + ✔️ + ✔️ + ✔️ Terminal - ✔️P - ✔️ - ❌ - ❌ - ✔️ - ✔️ - ❌ - ❌ - ✔️ - ✔️ - ❌ - ❌ - ❌ - ✔️ - ❌ - ❌ + ✔️P + ✔️ + ❌ + ❌ + ✔️ + ✔️ + ❌ + ❌ + ✔️ + ✔️ + ❌ + ❌ + ❌ + ❌ + ❌ + ❌ + ✔️ RepositoryR - ✔️ - ❌ - ❌ - ❌ - ❌ - ✔️ - ❌ - ✔️ - ✔️ - ✔️ - ❌ - ❌ - ❌ - ❌ - ❌ - ✔️ + ✔️ + ❌ + ❌ + ❌ + ❌ + ✔️ + ❌ + ✔️ + ✔️ + ✔️ + ❌ + ❌ + ❌ + ❌ + ❌ + ✔️ + ❌ @@ -1254,23 +1260,57 @@ By default, dates are based on the Greenwich meridian (England time). In order t -### 🎫 Gists +### 📰 Activity -The *gists* plugin displays your [gists](https://gist.github.com) metrics. + 🚧 This plugin is available as pre-release on @master branch (unstable) -![Gists plugin](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.gists.svg) +The *activity* plugin displays your recent activity in GitHub. + +![Activity plugin](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.activity.svg)
💬 About -It will consume an additional GitHub request per gist fetched. +It will consume an additional GitHub request. Add the following to your workflow : ```yaml -- uses: lowlighter/metrics@latest +- uses: lowlighter/metrics@master with: # ... other options - plugin_gists: yes + plugin_activity: yes + plugin_activity_limit: 5 + plugin_activity_days: 14 # Max age for events, set to 0 for unlimited +``` + +Metrics use data from [GitHub events](https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types) and is able to track the following events : + +| Event | Description | +| ------------ | ----------------------------------------------- | +| `push` | Push of commits | +| `issue` | Opening/Reopening/Closing of issues | +| `pr` | Opening/Closing of pull requests | +| `ref/create` | Creation of git tags or git branches | +| `ref/delete` | Deletion of git tags or git branches | +| `release` | Publication of new releases | +| `review` | Review of pull requests | +| `comment` | Comments on commits, issues and pull requests | +| `wiki` | Edition of wiki pages | +| `fork` | Forking of repositories | +| `star` | Starring of repositories | +| `public` | Repositories made public | +| `member` | Addition of new collaborator in repository | + +It is possible to filter the type of events you want to display by using `plugin_activity_filter` option. +Use the special value `"all"` (default value) to track all events. + +Add the following to your workflow : +```yaml +- uses: lowlighter/metrics@master + with: + # ... other options + plugin_activity: yes + plugin_activity_filter: issue, pr ```
@@ -1318,6 +1358,27 @@ Add the following to your workflow : +### 🎫 Gists + +The *gists* plugin displays your [gists](https://gist.github.com) metrics. + +![Gists plugin](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.gists.svg) + +
+💬 About + +It will consume an additional GitHub request per gist fetched. + +Add the following to your workflow : +```yaml +- uses: lowlighter/metrics@latest + with: + # ... other options + plugin_gists: yes +``` + +
+ ### 🔧 Other options A few additional options are available. @@ -1353,6 +1414,9 @@ It is possible to adjust the padding by adding the following to your workflow : Both positive and negative values are accepted, but you must specify a percentage. +If you specify a single value, it'll be used as for both width and height padding. +When two values are specified separated by a comma, the first one will be used for width and the second for height. + #### 💱 Convert output to PNG/JPEG It is possible to convert output from SVG to PNG or JPEG images by adding the following to your workflow : @@ -1390,3 +1454,4 @@ Read [CONTRIBUTING.md](CONTRIBUTING.md) for more information about this. * [jstrieb/github-stats](https://github.com/jstrieb/github-stats) * [ankurparihar/readme-pagespeed-insights](https://github.com/ankurparihar/readme-pagespeed-insights) * [jasonlong/isometric-contributions](https://github.com/jasonlong/isometric-contributions) +* [jamesgeorge007/github-activity-readme](https://github.com/jamesgeorge007/github-activity-readme) diff --git a/action.yml b/action.yml index 03e12b87..f952af5e 100644 --- a/action.yml +++ b/action.yml @@ -4,58 +4,61 @@ inputs: # Personal user token - # No additional scopes are needed, unless if you want to include private repositories metrics or use the traffic plugin + # No additional scopes are needed unless you want to include private repositories metrics + # Some plugins may also require additional scopes token: description: GitHub Personal Token required: true # Set to "${{ secrets.GITHUB_TOKEN }}" committer_token: - description: Token used for commits + description: GitHub Token used to commit metrics default: "" - # User to compute metrics - # Defaults to the owner of "token" + # GitHub username + # Optional, as it defaults "token"'s owner user: description: GitHub username default: "" - # Filepath of generated metrics (relative to repository root) + # Output path for generated metrics, relative to repository's root filename: description: Path of SVG image output default: github-metrics.svg - # Optimize SVG image with SVGO (minify and remove useless attributes and spaces) + # Optimize SVG image with SVGO + # It minifies and removes useless attributes # Some templates may not support this option optimize: - description: Optimize SVG image + description: SVG optimization default: yes - # Set timezone used by metrics + # Timezone used by metrics # See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones - # Some plugins will use this setting to calibrate dates + # Some plugins will use it to calibrate dates config_timezone: - description: Timezone used by metrics + description: Timezone used default: "" - # Specify output type + # Metrics output type # Supported values are : - # - svg (support animations and transparency) - # - png (support transparency) - # - jpeg + # - svg + # - png (does not support animations) + # - jpeg (does not support animations and transparency) config_output: description: Output image type default: svg - # Enable or disable SVG animations + # Enable or disable SVG CSS animations config_animations: - description: Enable or disable SVG animations + description: SVG CSS animations default: yes - # Add bottom padding (percentage) - # This can used to add padding if generated image is cropped or on the contrary, remove empty space + # Configure padding for output image (percentage) + # It can be used to add padding to generated metrics if rendering is cropped or has too much empty space + # You can specify one value (for both width and height) and two values (one for width and one for height, seperated by a comma) config_padding: - description: Configure bottom padding + description: Image padding default: 6% # Number of repositories to use for metrics @@ -363,6 +366,40 @@ inputs: description: Display stargazers evolution over the last two weeks default: no + # Display recent activity + plugin_activity: + description: Display recent activity + default: no + + # Number of activity events to display + # Capped to 100 + plugin_activity_limit: + description: Number of activity events to display + default: 5 + + # Disacard older events + # Use 0 to display activity whatever the date + plugin_activity_days: + description: Maximum activity event age + default: 14 + + # Events type to display + # Pass a string of comma-separated values + # Supported values are + # - "comment" for all kind of comments (commits, issue and pr) + # - "ref/create" and "ref/delete" for tag and branch creation/deletion + # - "release" for new published releases + # - "wiki" for wiki edition + # - "push" for pushed commits + # - "issue" and "pr" for issues and pull requests + # - "review" for pull requests review + # - "public" for repositories made public + # - "fork" and "star" for forked and starred repositories + # - "member" for accepted repository invitations + plugin_activity_filter: + description: Events to display + default: all + # ==================================================================================== # Options below are mostly used for testing @@ -417,7 +454,7 @@ branding: # The action will parse its name to check if it's the official action or if it's a forked one # On the official action, it'll use the docker image published on GitHub registry when using a released version, allowing faster runs -# On a forked action, it'll rebuild the docker image from Dockerfile +# On a forked action, it'll rebuild the docker image from Dockerfile to take into account changes you made runs: using: composite steps: @@ -458,7 +495,7 @@ runs: set +e METRICS_IS_RELEASED=$(expr $(expr match $METRICS_VERSION .*-beta) == 0) set -e - echo "Unreleased: $METRICS_IS_RELEASED" + echo "Is released version: $METRICS_IS_RELEASED" # Rebuild image for unreleased version if [[ $METRICS_IS_RELEASED ]]; then echo "Using released version $METRICS_TAG, will pull docker image from GitHub registry" diff --git a/settings.example.json b/settings.example.json index 815ce845..6049aed6 100644 --- a/settings.example.json +++ b/settings.example.json @@ -64,6 +64,9 @@ }, "stargazers":{ "//":"Stargazers plugin", "enabled":false, "//":"Enable or disable stargazers charts display" + }, + "activity":{ "//":"Activity plugin", + "enabled":false, "//":"Enable or disable recent activity display" } } } \ No newline at end of file diff --git a/source/app/action/index.mjs b/source/app/action/index.mjs index bbbbbcd5..e9f8d8ef 100644 --- a/source/app/action/index.mjs +++ b/source/app/action/index.mjs @@ -141,6 +141,7 @@ tweets:{enabled:input.bool("plugin_tweets")}, stars:{enabled:input.bool("plugin_stars")}, stargazers:{enabled:input.bool("plugin_stargazers")}, + activity:{enabled:input.bool("plugin_activity")}, } let q = Object.fromEntries(Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => [key, true])) info("Plugins enabled", Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key)) @@ -215,6 +216,13 @@ for (const option of ["limit"]) info(`Stars ${option}`, q[`stars.${option}`] = input.number(`plugin_stars_${option}`)) } + //Activity + if (plugins.activity.enabled) { + for (const option of ["limit", "days"]) + info(`Activity ${option}`, q[`activity.${option}`] = input.number(`plugin_activity_${option}`)) + for (const option of ["filter"]) + info(`Activity ${option}`, q[`activity.${option}`] = input.array(`plugin_activity_${option}`)) + } //Repositories to use const repositories = input.number("repositories") diff --git a/source/app/metrics.mjs b/source/app/metrics.mjs index 866e7e9a..ac349384 100644 --- a/source/app/metrics.mjs +++ b/source/app/metrics.mjs @@ -84,7 +84,7 @@ console.debug(`metrics/compute/${login} > render`) let rendered = await ejs.render(image, {...data, s, style, fonts}, {async:true}) //Apply resizing - const {resized, mime} = await svgresize(rendered, {padding:q["config.padding"], convert}) + const {resized, mime} = await svgresize(rendered, {paddings:q["config.padding"], convert}) rendered = resized //Additional SVG transformations @@ -180,16 +180,16 @@ } /** Render svg */ - async function svgresize(svg, {padding = "6%", convert} = {}) { + async function svgresize(svg, {paddings = "6%", convert} = {}) { //Instantiate browser if needed if (!svgresize.browser) { svgresize.browser = await puppeteer.launch({headless:true, executablePath:process.env.PUPPETEER_BROWSER_PATH, args:["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"]}) console.debug(`metrics/svgresize > started ${await svgresize.browser.version()}`) } //Format padding - padding = ((Number(`${padding}`.substring(0, padding.length-1))||0)/100) - console.debug(`metrics/svgresize > padding ${(100*padding).toFixed(2)}%`) - padding += 1 + const [pw = 1, ph] = paddings.split(",").map(padding => `${padding}`.substring(0, padding.length-1)).map(value => 1+Number(value)/100) + const padding = {width:pw, height:ph ?? pw} + console.debug(`metrics/svgresize > padding width*${padding.width}, height*${padding.height}`) //Render through browser and resize height const page = await svgresize.browser.newPage() await page.setContent(svg, {waitUntil:"load"}) @@ -201,8 +201,8 @@ document.querySelector("svg").classList.add("no-animations") //Get bounds and resize let {y:height, width} = document.querySelector("svg #metrics-end").getBoundingClientRect() - height = Math.ceil(height*padding) - width = Math.ceil(width) + height = Math.ceil(height*padding.height) + width = Math.ceil(width*padding.width) //Resize svg document.querySelector("svg").setAttribute("height", height) //Enable animations @@ -270,6 +270,7 @@ projects:{list:[...new Array(2).fill(null).map(() => ({name:"########", updated:"########", progress:{enabled:true, todo:"##", doing:"##", done:"##", total:"##"}}))]}, tweets:{profile:{username:"########", verified:false}, list:[...new Array(2).fill(null).map(() => ({text:"###### ###### ####### ######".repeat(4), created_at:Date.now()}))]}, stars:{repositories:[...new Array(2).fill({node:{nameWithOwner:"########/########", description:"###### ###### ####### ######".repeat(4)}})]}, + activity:{events:[{type:"comment", on:"pr"}, {type:"public"}, {type:"release"}, {type:"issue"}]} }[key]??{})] )), }) diff --git a/source/app/mocks.mjs b/source/app/mocks.mjs index 462f3818..33a18262 100644 --- a/source/app/mocks.mjs +++ b/source/app/mocks.mjs @@ -434,32 +434,316 @@ status:"200 OK", "x-oauth-scopes":"repo", }, - data:page < 1 ? new Array(10).fill(null).map(() => - (false ? { - id:"10000000001", - type:"IssueCommentEvent", - } : { + data:page < 1 ? [] : [ + { id:"10000000000", - type:"PushEvent", + type:"CommitCommentEvent", actor:{ - id:22963968, login:"lowlighter", }, - repo: { - id:293860197, + repo:{ name:"lowlighter/metrics", }, - payload: { - ref:"refs/heads/master", - commits: [ + payload:{ + comment:{ + user:{ + login:"lowlighter", + }, + path:"README.md", + commit_id:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + body:"This is a commit comment", + } + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000001", + type:"PullRequestReviewCommentEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/metrics", + }, + payload:{ + action:"created", + comment:{ + user:{ + login:"lowlighter", + }, + body:"This is pull request review comment", + }, + pull_request:{ + title:"Pull request example", + number:1, + user:{ + login:"lowlighter", + }, + body:"", + } + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000002", + type:"IssuesEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/metrics", + }, + payload:{ + action:"closed", + issue:{ + number:2, + title:"Issue example", + user:{ + login:"lowlighter", + }, + body:"Hello this is an example", + performed_via_github_app: null + } + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000003", + type:"GollumEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/lowlighter", + }, + payload:{ + pages:[ { - url:"https://api.github.com/repos/lowlighter/metrics/commits/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + page_name:"Home", + title:"Home", + summary:null, + action:"created", + sha:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", } ] }, - created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString() - }) - ) : [] + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000004", + type:"IssueCommentEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/metrics", + }, + payload:{ + action:"created", + issue:{ + number:3, + title:"Issue example", + user:{ + login:"lowlighter", + }, + labels:[ + { + name:"question", + color:"d876e3", + } + ], + state:"open", + }, + comment:{ + body:"Hello world !", + performed_via_github_app: null + } + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000005", + type:"ForkEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + id:327522930, + name:"lowlighter/gracidea", + }, + payload:{ + forkee:{ + name:"gracidea", + full_name:"lowlighter/gracidea", + } + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000006", + type:"PullRequestReviewEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/metrics", + }, + payload:{ + action:"created", + review:{ + user:{ + login:"lowlighter", + }, + state:"approved", + }, + pull_request:{ + state:"open", + number:4, + locked:false, + title:"Pull request example", + user:{ + login:"user", + }, + } + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000007", + type:"ReleaseEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/metrics", + }, + payload:{ + action:"published", + release:{ + tag_name:"v3.1", + name:"Version 3.1", + draft: false, + prerelease: true, + } + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000008", + type:"CreateEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/metrics", + }, + payload:{ + ref:"feat-new-plugin", + ref_type:"branch", + master_branch:"master", + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"100000000009", + type:"WatchEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/gracidea", + }, + payload:{action:"started"}, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000010", + type:"DeleteEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/metrics", + }, + payload:{ + ref:"feat-plugin-merged", + ref_type:"branch", + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000011", + type:"PushEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/metrics", + }, + payload:{ + size:1, + ref:"refs/heads/master", + commits:[ + { + sha:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + message:"Commit example", + } + ] + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000012", + type:"PullRequestEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/metrics", + }, + payload:{ + action:"opened", + number:5, + pull_request:{ + state:"open", + title:"Pull request example", + additions:210, + deletions:126, + changed_files:10, + } + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000013", + type:"MemberEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/metrics", + }, + payload:{ + member:{ + login:"botlighter", + }, + action:"added" + }, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + }, + { + id:"10000000014", + type:"PublicEvent", + actor:{ + login:"lowlighter", + }, + repo:{ + name:"lowlighter/metrics", + }, + payload:{}, + created_at:new Date(Date.now()-Math.floor(-Math.random()*14)*Math.floor(-Math.random()*24)*60*60*1000).toISOString(), + } + ] }) } }) diff --git a/source/app/web/instance.mjs b/source/app/web/instance.mjs index 72aabe6a..6e1209c0 100644 --- a/source/app/web/instance.mjs +++ b/source/app/web/instance.mjs @@ -72,6 +72,7 @@ const actions = {flush:new Map()} app.get("/", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`)) app.get("/index.html", limiter, (req, res) => res.sendFile(`${conf.statics}/index.html`)) + app.get("/favicon.ico", limiter, (req, res) => res.sendFile(`${conf.statics}/favicon.png`)) app.get("/.favicon.png", limiter, (req, res) => res.sendFile(`${conf.statics}/favicon.png`)) app.get("/.opengraph.png", limiter, (req, res) => res.sendFile(`${conf.statics}/opengraph.png`)) app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version)) diff --git a/source/app/web/statics/app.js b/source/app/web/statics/app.js index 2eb45f74..e57ac3a8 100644 --- a/source/app/web/statics/app.js +++ b/source/app/web/statics/app.js @@ -58,6 +58,7 @@ tweets:"🐤 Latest tweets", stars:"🌟 Recently starred repositories", stargazers:"✨ Stargazers over last weeks", + activity:"📰 Recent activity", "base.header":` Header`, @@ -95,6 +96,7 @@ "topics.limit":12, "tweets.limit":2, "stars.limit":4, + "activity.limit":5, }, }, templates:{ diff --git a/source/app/web/statics/index.html b/source/app/web/statics/index.html index 48f5e9b2..f449f21f 100644 --- a/source/app/web/statics/index.html +++ b/source/app/web/statics/index.html @@ -112,7 +112,7 @@ -
+

🔧 Configure plugins

@@ -231,6 +231,13 @@
+
+

{{ plugins.descriptions.activity }}

+ +
diff --git a/source/plugins/activity/index.mjs b/source/plugins/activity/index.mjs new file mode 100644 index 00000000..122dbfc3 --- /dev/null +++ b/source/plugins/activity/index.mjs @@ -0,0 +1,136 @@ +//Setup + export default async function ({login, rest, imports, q}, {enabled = false} = {}) { + //Plugin execution + try { + //Check if plugin is enabled and requirements are met + if ((!enabled)||(!q.activity)) + return null + + //Parameters override + let {"activity.limit":limit = 5, "activity.days":days = 7, "activity.filter":filter = "all"} = q + //Events + limit = Math.max(1, Math.min(100, Number(limit))) + //Days + days = Number(days) > 0 ? Number(days) : Infinity + //Filtered events + filter = decodeURIComponent(filter).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x) + + //Get user recent activity + console.debug(`metrics/compute/${login}/plugins > activity > querying api`) + const {data:events} = await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100}) + console.debug(`metrics/compute/${login}/plugins > activity > ${events.length} events loaded`) + //Extract activity events + const activity = events + .filter(({actor}) => actor.login === login) + .filter(({created_at}) => Number.isFinite(days) ? new Date(created_at) > new Date(Date.now()-days*24*60*60*1000) : true) + .map(({type, payload, repo:{name:repo}}) => { + //See https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types#memberevent + switch (type) { + //Commented on a commit + case "CommitCommentEvent":{ + if (!["created"].includes(payload.action)) + return null + const {comment:{user:{login:user}, commit_id:sha, body:content}} = payload + return {type:"comment", on:"commit", repo, content, user, mobile:null, number:sha.substring(0, 7), title:""} + } + //Created a git branch or tag + case "CreateEvent":{ + const {ref:name, ref_type:type} = payload + return {type:"ref/create", repo, ref:{name, type}} + } + //Deleted a git branch or tag + case "DeleteEvent":{ + const {ref:name, ref_type:type} = payload + return {type:"ref/delete", repo, ref:{name, type}} + } + //Forked repository + case "ForkEvent":{ + return {type:"fork", repo} + } + //Wiki editions + case "GollumEvent":{ + const {pages} = payload + return {type:"wiki", repo, pages:pages.map(({title}) => title)} + } + //Commented on an issue + case "IssueCommentEvent":{ + if (!["created"].includes(payload.action)) + return null + const {issue:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload + return {type:"comment", on:"issue", repo, content, user, mobile, number, title} + } + //Issue event + case "IssuesEvent":{ + if (!["opened", "closed", "reopened"].includes(payload.action)) + return null + const {action, issue:{user:{login:user}, title, number}} = payload + return {type:"issue", repo, action, user, number, title} + } + //Activity from repository collaborators + case "MemberEvent":{ + if (!["added"].includes(payload.action)) + return null + const {member:{login:user}} = payload + return {type:"member", repo, user} + } + //Made repository public + case "PublicEvent":{ + return {type:"public", repo} + } + //Pull requests events + case "PullRequestEvent":{ + if (!["opened", "closed"].includes(payload.action)) + return null + const {action, pull_request:{title, number, additions:added, deletions:deleted, changed_files:changed}} = payload + return {type:"pr", repo, action, title, number, lines:{added, deleted}, files:{changed}} + } + //Reviewed a pull request + case "PullRequestReviewEvent":{ + const {review:{state:review}, pull_request:{user:{login:user}, number, title}} = payload + return {type:"review", repo, review, user, number, title} + } + //Commented on a pull request + case "PullRequestReviewCommentEvent":{ + if (!["created"].includes(payload.action)) + return null + const {pull_request:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload + return {type:"comment", on:"pr", repo, content, user, mobile, number, title} + } + //Pushed commits + case "PushEvent":{ + const {size, commits, ref} = payload + return {type:"push", repo, size, branch:ref.match(/refs.heads.(?.*)/)?.groups?.branch ?? null, commits:commits.map(({sha, message}) => ({sha:sha.substring(0, 7), message}))} + } + //Released + case "ReleaseEvent":{ + if (!["published"].includes(payload.action)) + return null + const {action, release:{name, prerelease, draft}} = payload + return {type:"release", repo, action, name, prerelease, draft} + } + //Starred a repository + case "WatchEvent":{ + if (!["started"].includes(payload.action)) + return null + const {action} = payload + return {type:"star", repo, action} + } + //Unknown event + default:{ + return null + } + } + }) + .filter(event => event) + .filter(event => filter.includes("all") || filter.includes(event.type)) + .slice(0, limit) + + //Results + return {events:activity} + } + //Handle errors + catch (error) { + throw {error:{message:"An error occured", instance:error}} + } + } + diff --git a/source/plugins/habits/index.mjs b/source/plugins/habits/index.mjs index 9fa1924c..cb9301c1 100644 --- a/source/plugins/habits/index.mjs +++ b/source/plugins/habits/index.mjs @@ -10,7 +10,7 @@ //Events from = Math.max(1, Math.min(1000, Number(from))) //Days - days = Math.max(1, Math.min(30, Number(from))) + days = Math.max(1, Math.min(30, Number(days))) //Initialization const habits = {facts, charts, commits:{hour:NaN, hours:{}, day:NaN, days:{}}, indents:{style:"", spaces:0, tabs:0}, linguist:{available:false, ordered:[], languages:{}}} const pages = Math.ceil(from/100) diff --git a/source/plugins/pagespeed/index.mjs b/source/plugins/pagespeed/index.mjs index a4c01d8a..b8ef97b7 100644 --- a/source/plugins/pagespeed/index.mjs +++ b/source/plugins/pagespeed/index.mjs @@ -19,7 +19,7 @@ await Promise.all(["performance", "accessibility", "best-practices", "seo"].map(async category => { //Perform audit console.debug(`metrics/compute/${login}/plugins > pagespeed > performing audit ${category}`) - const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}&key=${token}`) + const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}${token ? `&key=${token}` : ""}`) console.debug(request.data) const {score, title} = request.data.lighthouseResult.categories[category] scores.set(category, {score, title}) @@ -34,7 +34,7 @@ //Detailed metrics if (detailed) { console.debug(`metrics/compute/${login}/plugins > pagespeed > performing detailed audit`) - const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?&url=${url}&key=${token}`) + const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?&url=${url}${token ? `&key=${token}` : ""}`) console.debug(request.data) Object.assign(result.metrics, ...request.data.lighthouseResult.audits.metrics.details.items) console.debug(`metrics/compute/${login}/plugins > pagespeed > performed detailed audit (status code ${request.status})`) diff --git a/source/templates/classic/image.svg b/source/templates/classic/image.svg index 86a8f7dd..613d0302 100644 --- a/source/templates/classic/image.svg +++ b/source/templates/classic/image.svg @@ -895,6 +895,152 @@ <% } %> + <% if (plugins.activity) { %> +
+

+ + Recent activity +

+
+
+ <% if (plugins.activity.error) { %> +
+ + <%= plugins.activity.error.message %> +
+ <% } else { %> + <% if (!plugins.activity.events.length) { %> +
+ + No recent activity +
+ <% } %> + <% for (const {type, repo, ...event} of plugins.activity.events) { %> +
+
+ <% if (/^ref/.test(type)) { %> +
+ <% if (event.ref.type === "branch") { %> + + <% } else { %> + + <% } %> + <%= /create/.test(type) ? "Created new" : "Deleted" %> + <%= event.ref.type %>
<%= event.ref.name %>
in
<%= repo %>
+
+ <% } %> + <% if (type === "comment") { %> +
+ <% if (event.on === "pr") { %> + + <% } else if ((event.on === "issue")||(event.on === "commit")) { %> + + <% } %> + Commented on
#<%= event.number %> <%= event.title %>
+
+
+
<%= event.on === "commit" ? "committed" : "opened" %> by <%= event.user %> in
<%= repo %>
+
<%= event.content %>
+
+ <% } %> + <% if (type === "wiki") { %> +
+ + Updated <%= event.pages.length %> wiki page<%= s(event.pages.length) %> in
<%= repo %>
+
+
+ <% for (const page of event.pages) { %> +
+ <%= page %> +
+ <% } %> +
+ <% } %> + <% if (type === "pr") { %> +
+ + <%= event.action === "opened" ? "Opened" : "Merged" %>
#<%= event.number %> <%= event.title %>
+
+
+
opened <%= user.login !== event.user ? `by ${event.user}` : "" %> in
<%= repo %>
+
<%= event.files.changed %> file<%= s(event.files.changed) %> changed
++<%= event.lines.added %> --<%= event.lines.deleted%>
+
+ <% } %> + <% if (type === "issue") { %> +
+ + <%= event.action === "opened" ? "Opened" : event.action === "reopened" ? "Reopened" : "Closed" %>
#<%= event.number %> <%= event.title %>
+
+
+
opened <%= user.login !== event.user ? `by ${event.user}` : "" %> in
<%= repo %>
+
+ <% } %> + <% if (type === "fork") { %> +
+ + Forked
<%= repo %>
+
+ <% } %> + <% if (type === "public") { %> +
+ + Made
<%= repo %>
public +
+ <% } %> + <% if (type === "review") { %> +
+ + Reviewed
#<%= event.number %> <%= event.title %>
+
+
+
opened <%= user.login !== event.user ? `by ${event.user}` : "" %> in
<%= repo %>
+
+ <% } %> + <% if (type === "push") { %> +
+ + Pushed <%= event.size %> commit<%= s(event.size) %> in
<%= repo %>
+
+
+ <% if (event.branch) { %> +
on branch
<%= event.branch %>
+ <% } %> + <% for (const commit of event.commits) { %> +
+
#<%= commit.sha %>
+
<%= commit.message %>
+
+ <% } %> +
+ <% } %> + <% if (type === "release") { %> +
+ + <%= event.draft ? "Drafted release" : event.prerelease ? "Pre-released" : "Released" %> +
<%= event.name %>
of
<%= repo %>
+
+ <% } %> + <% if (type === "star") { %> +
+ + Starred
<%= repo %>
+
+ <% } %> + <% if (type === "member") { %> +
+ + Added <%= event.user %> as collaborator in
<%= repo %>
+
+ <% } %> +
+
+ <% } %> + <% } %> +
+
+
+ <% } %> + <% if (base.metadata) { %>