Feat miscelleanous 1 (#28)
* Improve logs and better handling of plugins errors * Add support for timezones * Prepare next release
This commit is contained in:
42
README.md
42
README.md
@@ -213,14 +213,19 @@ The `README.md` of it will be displayed on your user profile :
|
||||
|
||||
From the `Developer settings` of your account settings, select `Personal access tokens` to create a new token.
|
||||
|
||||
No additional scopes are needed, unless you want to include your private repositories metrics or if you want to use the `traffic` plugin.
|
||||
No additional scopes are needed, unless you want to include your private repositories metrics.
|
||||
|
||||

|
||||
|
||||
Be sure to enable `Include private contributions on my profile` in your account settings if you want to include your private contributions :
|
||||
With a scope-less token, you can still display private contributions by enabling `Include private contributions on my profile` in your account settings :
|
||||
|
||||

|
||||
|
||||
Some plugins also require additional scopes, which is indicated in their respective documentation.
|
||||
In case your token does not have the required scope (and `plugins_errors_fatal` is not enabled), it will be directly notified in the plugin render like below :
|
||||
|
||||

|
||||
|
||||
### 2. Set your GitHub token in your personal repository secrets
|
||||
|
||||
Go to the `Settings` of your personal repository to create a new secret and paste your freshly generated GitHub token there.
|
||||
@@ -258,8 +263,6 @@ When using a token with additional permissions, it is advised to fork this repos
|
||||
```
|
||||
In this case, consider watching new releases of this repository to stay up-to-date and enjoy latest features !
|
||||
|
||||
If you prefer examples rather than theory, check out this [workflow](https://github.com/lowlighter/lowlighter/blob/master/.github/workflows/metrics.yml) file which generates metrics daily.
|
||||
|
||||
#### Preview vs release
|
||||
|
||||
It is possible to use `@master` instead of `@latest` to use new features before their official release.
|
||||
@@ -271,6 +274,12 @@ A new metrics image will be generated and committed to your repository on each r
|
||||
|
||||

|
||||
|
||||
#### Workflow examples
|
||||
|
||||
Check out this [workflow](https://github.com/lowlighter/lowlighter/blob/master/.github/workflows/metrics.yml) file which generates metrics daily.
|
||||
|
||||
Note that most of steps presented there are illustrative examples for this readme and are actually not needed to generate your own metrics.
|
||||
|
||||
### 4. Embed the link into your README.md
|
||||
|
||||
Edit your repository readme and add your metrics image :
|
||||
@@ -867,6 +876,8 @@ Add the following to your workflow :
|
||||
|
||||
### 🗂️ Projects
|
||||
|
||||
⚠️ This plugin requires a personal token with public_repo scope.
|
||||
|
||||
The *projects* plugin displays the progress of your profile projects.
|
||||
|
||||

|
||||
@@ -876,6 +887,8 @@ The *projects* plugin displays the progress of your profile projects.
|
||||
|
||||
It will consume an additional GitHub request.
|
||||
|
||||
Because of GitHub REST API limitation, provided token requires `public_repo` scope to access projects informations.
|
||||
|
||||
Add the following to your workflow :
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
@@ -926,7 +939,7 @@ Add the following to your workflow :
|
||||
|
||||
### 🧮 Traffic
|
||||
|
||||
⚠️ This plugin requires a personal token with full repo scope.
|
||||
⚠️ This plugin requires a personal token with repo scope.
|
||||
|
||||
The repositories *traffic* plugin displays the number of pages views across your repositories.
|
||||
|
||||
@@ -955,8 +968,6 @@ Add the following to your workflow :
|
||||
|
||||
### 🐤 Tweets
|
||||
|
||||
🚧 This plugin is available as pre-release on @master
|
||||
|
||||
The recent *tweets* plugin displays your latest tweets of the [twitter](https://twitter.com) attached mentioned on your account :
|
||||
|
||||

|
||||
@@ -1030,7 +1041,7 @@ If you're using GitHub Api in other projects, you could reach the rate limit.
|
||||

|
||||
|
||||
These facts are generated from your recent coding activity.
|
||||
The indent style is deduced from the diffs of your recent commits.
|
||||
The indent style is deduced from the diffs of your recent commits.
|
||||
|
||||
Add the following to your workflow :
|
||||
```yaml
|
||||
@@ -1042,14 +1053,12 @@ Add the following to your workflow :
|
||||
plugin_habits_days: 14
|
||||
```
|
||||
|
||||
🚧 The following feature is available as pre-release on @master
|
||||
|
||||
You can display charts in this section :
|
||||
|
||||

|
||||
|
||||
These charts are generated from your recent coding activity.
|
||||
Languages metrics are computed with [github/linguist](https://github.com/github/linguist) from the diffs of your recent commits.
|
||||
These charts are generated from your recent coding activity.
|
||||
Languages metrics are computed with [github/linguist](https://github.com/github/linguist) from the diffs of your recent commits.
|
||||
|
||||
Add the following to your workflow instead :
|
||||
```yaml
|
||||
@@ -1063,6 +1072,15 @@ Add the following to your workflow instead :
|
||||
plugin_habits_charts: yes
|
||||
```
|
||||
|
||||
By default, dates are based on the Greenwich meridian (England time). In order to these metrics to be accurate, be sure to set your timezone (see [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) for a list of supported timezones) :
|
||||
|
||||
```yaml
|
||||
- uses: lowlighter/metrics@latest
|
||||
with:
|
||||
# ... other options
|
||||
config_timezone: Europe/Paris
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
### 🎫 Gists
|
||||
|
||||
@@ -40,6 +40,13 @@ inputs:
|
||||
description: Optimize SVG image
|
||||
default: yes
|
||||
|
||||
# Set 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
|
||||
config_timezone:
|
||||
description: Timezone used by metrics
|
||||
default: ""
|
||||
|
||||
# Number of repositories to use for metrics
|
||||
# A high number increase metrics accuracy, but will consume additional API requests when using plugins
|
||||
repositories:
|
||||
|
||||
24
action/dist/index.js
vendored
24
action/dist/index.js
vendored
File diff suppressed because one or more lines are too long
@@ -37,7 +37,7 @@
|
||||
console.log(`Template to use | ${template}`)
|
||||
|
||||
//Token for data gathering
|
||||
const token = core.getInput("token")
|
||||
const token = core.getInput("token") || ""
|
||||
console.log(`Github token | ${token ? "provided" : "missing"}`)
|
||||
if (!token)
|
||||
throw new Error("You must provide a valid GitHub token to gather your metrics")
|
||||
@@ -71,16 +71,22 @@
|
||||
if (!debug)
|
||||
console.debug = message => debugged.push(message)
|
||||
console.log(`Debug mode | ${debug}`)
|
||||
const dflags = (core.getInput("debug_flags") ?? "").split(" ").filter(flag => flag)
|
||||
const dflags = (core.getInput("debug_flags") || "").split(" ").filter(flag => flag)
|
||||
console.log(`Debug flags | ${dflags.join(" ")}`)
|
||||
|
||||
//Base elements
|
||||
const base = {}
|
||||
let parts = (core.getInput("base")||"").split(",").map(part => part.trim())
|
||||
let parts = (core.getInput("base") || "").split(",").map(part => part.trim())
|
||||
for (const part of conf.settings.plugins.base.parts)
|
||||
base[`base.${part}`] = parts.includes(part)
|
||||
console.log(`Base parts | ${parts.join(", ") || "(none)"}`)
|
||||
|
||||
//Config
|
||||
const config = {
|
||||
"config.timezone":core.getInput("config_timezone") || ""
|
||||
}
|
||||
console.log(`Timezone | ${config.timezone || "(none)"}`)
|
||||
|
||||
//Additional plugins
|
||||
const plugins = {
|
||||
lines:{enabled:bool(core.getInput("plugin_lines"))},
|
||||
@@ -102,7 +108,7 @@
|
||||
//Additional plugins options
|
||||
//Pagespeed
|
||||
if (plugins.pagespeed.enabled) {
|
||||
plugins.pagespeed.token = core.getInput("plugin_pagespeed_token")
|
||||
plugins.pagespeed.token = core.getInput("plugin_pagespeed_token") || ""
|
||||
q[`pagespeed.detailed`] = bool(core.getInput(`plugin_pagespeed_detailed`))
|
||||
console.log(`Pagespeed token | ${plugins.pagespeed.token ? "provided" : "missing"}`)
|
||||
console.log(`Pagespeed detailed | ${q["pagespeed.detailed"]}`)
|
||||
@@ -145,7 +151,7 @@
|
||||
}
|
||||
//Isocalendar
|
||||
if (plugins.isocalendar.enabled) {
|
||||
q["isocalendar.duration"] = core.getInput("plugin_isocalendar_duration") ?? "half-year"
|
||||
q["isocalendar.duration"] = core.getInput("plugin_isocalendar_duration") || "half-year"
|
||||
console.log(`Isocalendar duration | ${q["isocalendar.duration"]}`)
|
||||
}
|
||||
//Topics
|
||||
@@ -179,7 +185,7 @@
|
||||
console.log(`Plugin errors | ${die ? "die" : "ignore"}`)
|
||||
|
||||
//Built query
|
||||
q = {...q, base:false, ...base, repositories, template}
|
||||
q = {...q, base:false, ...base, ...config, repositories, template}
|
||||
|
||||
//Render metrics
|
||||
const rendered = await metrics({login:user, q, dflags}, {graphql, rest, plugins, conf, die})
|
||||
@@ -206,7 +212,7 @@
|
||||
console.log(`Repository | ${github.context.repo.owner}/${github.context.repo.repo}`)
|
||||
console.log(`Branch | ${branch}`)
|
||||
//Committer token
|
||||
const token = core.getInput("committer_token") || core.getInput("token")
|
||||
const token = core.getInput("committer_token") || core.getInput("token") || ""
|
||||
console.log(`Committer token | ${token ? "provided" : "missing"}`)
|
||||
if (!token)
|
||||
throw new Error("You must provide a valid GitHub token to commit your metrics")
|
||||
@@ -231,7 +237,7 @@
|
||||
)
|
||||
sha = oid
|
||||
} catch (error) { console.debug(error) }
|
||||
console.log(`Previous render sha | ${sha || "none"}`)
|
||||
console.log(`Previous render sha | ${sha ?? "none"}`)
|
||||
//Update file content through API
|
||||
await rest.repos.createOrUpdateFileContents({
|
||||
...github.context.repo, path:filename, message:`Update ${filename} - [Skip GitHub Action]`,
|
||||
@@ -245,8 +251,9 @@
|
||||
console.log(`Success !`)
|
||||
process.exit(0)
|
||||
|
||||
}
|
||||
//Errors
|
||||
} catch (error) {
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
if (!bool(core.getInput("debug")))
|
||||
for (const log of ["_".repeat(64), "An error occured, logging debug message :", ...debugged])
|
||||
|
||||
62
package-lock.json
generated
62
package-lock.json
generated
@@ -211,26 +211,26 @@
|
||||
}
|
||||
},
|
||||
"@babel/traverse": {
|
||||
"version": "7.12.10",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.10.tgz",
|
||||
"integrity": "sha512-6aEtf0IeRgbYWzta29lePeYSk+YAFIC3kyqESeft8o5CkFlYIMX+EQDDWEiAQ9LHOA3d0oHdgrSsID/CKqXJlg==",
|
||||
"version": "7.12.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.12.tgz",
|
||||
"integrity": "sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/generator": "^7.12.10",
|
||||
"@babel/helper-function-name": "^7.10.4",
|
||||
"@babel/helper-split-export-declaration": "^7.11.0",
|
||||
"@babel/parser": "^7.12.10",
|
||||
"@babel/types": "^7.12.10",
|
||||
"@babel/code-frame": "^7.12.11",
|
||||
"@babel/generator": "^7.12.11",
|
||||
"@babel/helper-function-name": "^7.12.11",
|
||||
"@babel/helper-split-export-declaration": "^7.12.11",
|
||||
"@babel/parser": "^7.12.11",
|
||||
"@babel/types": "^7.12.12",
|
||||
"debug": "^4.1.0",
|
||||
"globals": "^11.1.0",
|
||||
"lodash": "^4.17.19"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.12.11",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.11.tgz",
|
||||
"integrity": "sha512-ukA9SQtKThINm++CX1CwmliMrE54J6nIYB5XTwL5f/CLFW9owfls+YSU8tVW15RQ2w+a3fSbPjC6HdQNtWZkiA==",
|
||||
"version": "7.12.12",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.12.tgz",
|
||||
"integrity": "sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.12.11",
|
||||
@@ -280,9 +280,9 @@
|
||||
}
|
||||
},
|
||||
"@octokit/openapi-types": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.0.tgz",
|
||||
"integrity": "sha512-J4bfM7lf8oZvEAdpS71oTvC1ofKxfEZgU5vKVwzZKi4QPiL82udjpseJwxPid9Pu2FNmyRQOX4iEj6W1iOSnPw=="
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.0.1.tgz",
|
||||
"integrity": "sha512-9AuC04PUnZrjoLiw3uPtwGh9FE4Q3rTqs51oNlQ0rkwgE8ftYsOC+lsrQyvCvWm85smBbSc0FNRKKumvGyb44Q=="
|
||||
},
|
||||
"@octokit/plugin-paginate-rest": {
|
||||
"version": "2.6.2",
|
||||
@@ -343,18 +343,18 @@
|
||||
}
|
||||
},
|
||||
"@octokit/types": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.1.tgz",
|
||||
"integrity": "sha512-btm3D6S7VkRrgyYF31etUtVY/eQ1KzrNRqhFt25KSe2mKlXuLXJilglRC6eDA2P6ou94BUnk/Kz5MPEolXgoiw==",
|
||||
"version": "6.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.1.2.tgz",
|
||||
"integrity": "sha512-LPCpcLbcky7fWfHCTuc7tMiSHFpFlrThJqVdaHgowBTMS0ijlZFfonQC/C1PrZOjD4xRCYgBqH9yttEATGE/nw==",
|
||||
"requires": {
|
||||
"@octokit/openapi-types": "^2.0.0",
|
||||
"@octokit/openapi-types": "^2.0.1",
|
||||
"@types/node": ">= 8"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "14.14.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.14.tgz",
|
||||
"integrity": "sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ=="
|
||||
"version": "14.14.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.16.tgz",
|
||||
"integrity": "sha512-naXYePhweTi+BMv11TgioE2/FXU4fSl29HAH1ffxVciNsH3rYXjNP2yM8wqmSm7jS20gM8TIklKiTen+1iVncw=="
|
||||
},
|
||||
"@types/q": {
|
||||
"version": "1.5.4",
|
||||
@@ -371,9 +371,9 @@
|
||||
}
|
||||
},
|
||||
"@vercel/ncc": {
|
||||
"version": "0.25.1",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.25.1.tgz",
|
||||
"integrity": "sha512-dGecC5+1wLof1MQpey4+6i2KZv4Sfs6WfXkl9KfO32GED4ZPiKxRfvtGPjbjZv0IbqMl6CxtcV1RotXYfd5SSA==",
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.26.1.tgz",
|
||||
"integrity": "sha512-iVhYAL/rpHgjO88GkDHNVNrp7WTfMFBbeWXNgwaDPMv5rDI4hNOAM0u+Zhtbs42XBQE6EccNaY6UDb/tm1+dhg==",
|
||||
"dev": true
|
||||
},
|
||||
"abbrev": {
|
||||
@@ -445,9 +445,9 @@
|
||||
"integrity": "sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz",
|
||||
"integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
|
||||
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.10.0"
|
||||
}
|
||||
@@ -1416,9 +1416,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"get-intrinsic": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.1.tgz",
|
||||
"integrity": "sha512-ZnWP+AmS1VUaLgTRy47+zKtjTxz+0xMpx3I52i+aalBK1QP19ggLF3Db89KJX7kjfOfP2eoa01qc++GwPgufPg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz",
|
||||
"integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==",
|
||||
"requires": {
|
||||
"function-bind": "^1.1.1",
|
||||
"has": "^1.0.3",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "metrics",
|
||||
"version": "2.9.0-beta",
|
||||
"version": "2.9.0",
|
||||
"description": "An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !",
|
||||
"main": "index.mjs",
|
||||
"scripts": {
|
||||
@@ -24,7 +24,7 @@
|
||||
"@actions/github": "^4.0.0",
|
||||
"@octokit/graphql": "^4.5.8",
|
||||
"@octokit/rest": "^18.0.12",
|
||||
"axios": "^0.21.0",
|
||||
"axios": "^0.21.1",
|
||||
"colors": "^1.4.0",
|
||||
"compression": "^1.7.4",
|
||||
"ejs": "^3.1.5",
|
||||
@@ -39,7 +39,7 @@
|
||||
"vue-prism-component": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vercel/ncc": "^0.25.1",
|
||||
"@vercel/ncc": "^0.26.1",
|
||||
"babel-minify": "^0.5.1",
|
||||
"libxmljs": "^0.19.7"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import setup from "./setup.mjs"
|
||||
import metrics from "./metrics.mjs"
|
||||
import Templates from "./templates/index.mjs"
|
||||
import util from "util"
|
||||
|
||||
/** App */
|
||||
export default async function () {
|
||||
@@ -107,7 +108,7 @@
|
||||
//Compute rendering
|
||||
try {
|
||||
//Render
|
||||
console.debug(`metrics/app/${login} > ${JSON.stringify(req.query)}`)
|
||||
console.debug(`metrics/app/${login} > ${util.inspect(req.query, {depth:Infinity, maxStringLength:256})}`)
|
||||
const rendered = await metrics({login, q:parse(req.query)}, {graphql, rest, plugins, conf})
|
||||
//Cache
|
||||
if ((!debug)&&(cached)&&(login !== "placeholder"))
|
||||
@@ -140,7 +141,7 @@
|
||||
`Debug mode | ${debug}`,
|
||||
`Restricted to users | ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
|
||||
`Cached time | ${cached} seconds`,
|
||||
`Rate limiter | ${ratelimiter ? JSON.stringify(ratelimiter) : "(enabled)"}`,
|
||||
`Rate limiter | ${ratelimiter ? util.inspect(req.query, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`,
|
||||
`Max simultaneous users | ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
|
||||
`Plugins enabled | ${enabled.join(", ")}`
|
||||
].join("\n")))
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import fs from "fs/promises"
|
||||
import os from "os"
|
||||
import paths from "path"
|
||||
import util from "util"
|
||||
|
||||
//Setup
|
||||
export default async function metrics({login, q, dflags = []}, {graphql, rest, plugins, conf, die = false}) {
|
||||
@@ -19,7 +20,7 @@
|
||||
|
||||
//Init
|
||||
console.debug(`metrics/compute/${login} > start`)
|
||||
console.debug(JSON.stringify(q))
|
||||
console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256}))
|
||||
const template = q.template || conf.settings.templates.default
|
||||
const repositories = Math.max(0, Number(q.repositories)) || conf.settings.repositories || 100
|
||||
const pending = []
|
||||
@@ -27,7 +28,7 @@
|
||||
if ((!(template in Templates))||(!(template in conf.templates))||((conf.settings.templates.enabled.length)&&(!conf.settings.templates.enabled.includes(template))))
|
||||
throw new Error("unsupported template")
|
||||
const {query, image, style, fonts} = conf.templates[template]
|
||||
const data = {base:{}}
|
||||
const data = {base:{}, config:{}}
|
||||
|
||||
//Base parts
|
||||
{
|
||||
@@ -53,13 +54,10 @@
|
||||
//Compute metrics
|
||||
console.debug(`metrics/compute/${login} > compute`)
|
||||
const computer = Templates[template].default || Templates[template]
|
||||
await computer({login, q, dflags}, {conf, data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, format, bytes, shuffle, htmlescape, urlexpand}})
|
||||
await computer({login, q, dflags}, {conf, data, rest, graphql, plugins}, {s, pending, imports:{plugins:Plugins, url, imgb64, axios, puppeteer, run, fs, os, paths, util, format, bytes, shuffle, htmlescape, urlexpand}})
|
||||
const promised = await Promise.all(pending)
|
||||
|
||||
//Check plugins errors
|
||||
if (conf.settings.debug)
|
||||
for (const {name, result = null} of promised)
|
||||
console.debug(`plugin ${name} ${result ? result.error ? "failed" : "success" : "ignored"} : ${JSON.stringify(result).replace(/^(.{888}).+/, "$1...")}`)
|
||||
if (die) {
|
||||
const errors = promised.filter(({result = null}) => !!result?.error).length
|
||||
if (errors)
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.gists))
|
||||
return null
|
||||
//Retrieve contribution calendar from graphql api
|
||||
//Retrieve gists from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > gists > querying api`)
|
||||
const {user:{gists}} = await graphql(`
|
||||
query Gists {
|
||||
user(login: "${login}") {
|
||||
@@ -27,6 +28,7 @@
|
||||
`
|
||||
)
|
||||
//Iterate through gists
|
||||
console.debug(`metrics/compute/${login}/plugins > gists > processing ${gists.nodes.length} gists`)
|
||||
let stargazers = 0, forks = 0, comments = 0
|
||||
for (const gist of gists.nodes) {
|
||||
//Skip forks
|
||||
@@ -42,7 +44,6 @@
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//Setup
|
||||
export default async function ({login, rest, imports, q}, {enabled = false, from:defaults = 100} = {}) {
|
||||
export default async function ({login, rest, imports, data, q}, {enabled = false, from:defaults = 100} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
@@ -14,23 +14,25 @@
|
||||
//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)
|
||||
const offset = data.config.timezone?.offset ?? 0
|
||||
//Get user recent activity
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > querying api`)
|
||||
const events = []
|
||||
try {
|
||||
for (let page = 0; page < pages; page++) {
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > loaded page ${page}`)
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > loading page ${page}`)
|
||||
events.push(...(await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data)
|
||||
}
|
||||
} catch { console.debug(`metrics/compute/${login}/plugins > habits > no more events to load`) }
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > no more events to load (${events.length} loaded)`)
|
||||
} catch { console.debug(`metrics/compute/${login}/plugins > habits > no more page to load`) }
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > ${events.length} events loaded`)
|
||||
//Get user recent commits
|
||||
const commits = events
|
||||
.filter(({type}) => type === "PushEvent")
|
||||
.filter(({actor}) => actor.login === login)
|
||||
.filter(({created_at}) => new Date(created_at) > new Date(Date.now()-days*24*60*60*1000))
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > filtered out ${commits.length} commits`)
|
||||
const actor = commits[0]?.actor?.id ?? 0
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > filtered out ${commits.length} push events over last ${days} days`)
|
||||
//Retrieve edited files and filter edited lines (those starting with +/-) from patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > loading patches`)
|
||||
const patches = [...await Promise.allSettled(commits
|
||||
.flatMap(({payload}) => payload.commits).map(commit => commit.url)
|
||||
.map(async commit => (await rest.request(commit)).data.files)
|
||||
@@ -42,7 +44,8 @@
|
||||
//Commit day
|
||||
{
|
||||
//Compute commit days
|
||||
const days = commits.map(({created_at}) => (new Date(created_at)).getDay())
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching most active day of week`)
|
||||
const days = commits.map(({created_at}) => (new Date(new Date(created_at).getTime() + offset)).getDay())
|
||||
for (const day of days)
|
||||
habits.commits.days[day] = (habits.commits.days[day] ?? 0) + 1
|
||||
habits.commits.days.max = Math.max(...Object.values(habits.commits.days))
|
||||
@@ -52,7 +55,8 @@
|
||||
//Commit hour
|
||||
{
|
||||
//Compute commit hours
|
||||
const hours = commits.map(({created_at}) => (new Date(created_at)).getHours())
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching most active time of day`)
|
||||
const hours = commits.map(({created_at}) => (new Date(new Date(created_at).getTime() + offset)).getHours())
|
||||
for (const hour of hours)
|
||||
habits.commits.hours[hour] = (habits.commits.hours[hour] ?? 0) + 1
|
||||
habits.commits.hours.max = Math.max(...Object.values(habits.commits.hours))
|
||||
@@ -62,6 +66,7 @@
|
||||
//Indent style
|
||||
{
|
||||
//Attempt to guess whether tabs or spaces are used in patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching indent style`)
|
||||
patches
|
||||
.map(({patch}) => patch.match(/((?:\t)|(?: )) /gm) ?? [])
|
||||
.forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++)
|
||||
@@ -70,36 +75,38 @@
|
||||
//Linguist
|
||||
if (charts) {
|
||||
//Check if linguist exists
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching recently used languages using linguist`)
|
||||
const prefix = {win32:"wsl"}[process.platform] ?? ""
|
||||
if ((patches.length)&&(await imports.run(`${prefix} which github-linguist`))) {
|
||||
//Setup for linguist
|
||||
habits.linguist.available = true
|
||||
const path = imports.paths.join(imports.os.tmpdir(), `${actor}`)
|
||||
const path = imports.paths.join(imports.os.tmpdir(), `${commits[0]?.actor?.id ?? 0}`)
|
||||
//Create temporary directory and save patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > creating temp dir ${path} with ${patches.length} files`)
|
||||
await imports.fs.mkdir(path, {recursive:true})
|
||||
await Promise.all(patches.map(async ({name, patch}, i) => await imports.fs.writeFile(imports.paths.join(path, `${i}${imports.paths.extname(name)}`), patch)))
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > created temp dir ${path} with ${patches.length} files`)
|
||||
//Create temporary git repository
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > creating temp git repository`)
|
||||
await imports.run(`git init && git add . && git config user.name "linguist" && git config user.email "null@github.com" && git commit -m "linguist"`, {cwd:path}).catch(console.debug)
|
||||
await imports.run(`git status`, {cwd:path})
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > created temp git repository`)
|
||||
//Spawn linguist process
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > running linguist`)
|
||||
;(await imports.run(`${prefix} github-linguist`, {cwd:path}))
|
||||
;(await imports.run(`${prefix} github-linguist --breakdown`, {cwd:path}))
|
||||
//Parse linguist result
|
||||
.split("\n").map(line => line.match(/(?<value>[\d.]+)%\s+(?<language>\w+)/)?.groups).filter(line => line)
|
||||
.map(({value, language}) => habits.linguist.languages[language] = (habits.linguist.languages[language] ?? 0) + value/100)
|
||||
habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([an, a], [bn, b]) => b - a)
|
||||
}
|
||||
else
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > linguist is not available`)
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > linguist not available`)
|
||||
}
|
||||
//Results
|
||||
return habits
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,10 @@
|
||||
const padding = new Date(start)
|
||||
padding.setHours(-14*24)
|
||||
//Retrieve contribution calendar from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > querying api`)
|
||||
const calendar = {}
|
||||
for (const [name, from, to] of [["padding", padding, start], ["weeks", start, now]]) {
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > loading "${name}" from "${from.toISOString()}" to "${to.toISOString()}"`)
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > loading ${name} from "${from.toISOString()}" to "${to.toISOString()}"`)
|
||||
const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(`
|
||||
query Calendar {
|
||||
user(login: "${login}") {
|
||||
@@ -44,11 +45,13 @@
|
||||
calendar[name] = weeks
|
||||
}
|
||||
//Apply padding
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > applying padding`)
|
||||
const firstweek = calendar.weeks[0].contributionDays
|
||||
const padded = calendar.padding.flatMap(({contributionDays}) => contributionDays).filter(({date}) => !firstweek.map(({date}) => date).includes(date))
|
||||
while (firstweek.length < 7)
|
||||
firstweek.unshift(padded.pop())
|
||||
//Compute the highest contributions in a day, streaks and average commits per day
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing stats`)
|
||||
let max = 0, streak = {max:0, current:0}, values = [], average = 0
|
||||
for (const week of calendar.weeks) {
|
||||
for (const day of week.contributionDays) {
|
||||
@@ -60,6 +63,7 @@
|
||||
}
|
||||
average = (values.reduce((a, b) => a + b, 0)/values.length).toFixed(2).replace(/[.]0+$/, "")
|
||||
//Compute SVG
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing svg render`)
|
||||
const size = 6
|
||||
let i = 0, j = 0
|
||||
let svg = `
|
||||
@@ -96,7 +100,6 @@
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
//Skipped repositories
|
||||
skipped = decodeURIComponent(skipped).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x)
|
||||
//Iterate through user's repositories and retrieve languages data
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > processing ${data.user.repositories.nodes.length} repositories`)
|
||||
const languages = {colors:{}, total:0, stats:{}}
|
||||
for (const repository of data.user.repositories.nodes) {
|
||||
//Skip repository if asked
|
||||
@@ -33,6 +34,7 @@
|
||||
}
|
||||
}
|
||||
//Compute languages stats
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > computing stats`)
|
||||
Object.keys(languages.stats).map(name => languages.stats[name] /= languages.total)
|
||||
languages.favorites = Object.entries(languages.stats).sort(([an, a], [bn, b]) => b - a).slice(0, 8).map(([name, value]) => ({name, value, color:languages.colors[name], x:0}))
|
||||
for (let i = 1; i < languages.favorites.length; i++)
|
||||
@@ -42,7 +44,6 @@
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,11 @@
|
||||
//Repositories
|
||||
const repositories = data.user.repositories.nodes.map(({name}) => name) ?? []
|
||||
//Get contributors stats from repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > lines > querying api`)
|
||||
const lines = {added:0, deleted:0}
|
||||
const response = await Promise.all(repositories.map(async repo => await rest.repos.getContributorsStats({owner:login, repo})))
|
||||
//Compute changed lines
|
||||
console.debug(`metrics/compute/${login}/plugins > lines > computing total diff`)
|
||||
response.map(({data:repository}) => {
|
||||
//Check if data are available
|
||||
if (!Array.isArray(repository))
|
||||
@@ -29,8 +31,7 @@
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,13 +56,14 @@
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(100, Number(limit)))
|
||||
//Handle mode
|
||||
console.debug(`metrics/compute/${login}/plugins > music > processing mode ${mode} with provider ${provider}`)
|
||||
switch (mode) {
|
||||
//Playlist mode
|
||||
case "playlist":{
|
||||
//Start puppeteer and navigate to playlist
|
||||
console.debug(`metrics/compute/${login}/plugins > music > starting browser`)
|
||||
const browser = await imports.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/compute/${login}/plugins > music > loaded ${await browser.version()}`)
|
||||
console.debug(`metrics/compute/${login}/plugins > music > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
console.debug(`metrics/compute/${login}/plugins > music > loading page`)
|
||||
await page.goto(playlist)
|
||||
@@ -103,7 +104,7 @@
|
||||
if (Array.isArray(tracks)) {
|
||||
//Tracks
|
||||
console.debug(`metrics/compute/${login}/plugins > music > found ${tracks.length} tracks`)
|
||||
console.debug(JSON.stringify(tracks))
|
||||
console.debug(imports.util.inspect(tracks, {depth:Infinity, maxStringLength:256}))
|
||||
//Shuffle tracks
|
||||
tracks = imports.shuffle(tracks)
|
||||
}
|
||||
@@ -120,17 +121,18 @@
|
||||
//Prepare credentials
|
||||
const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim())
|
||||
if ((!client_id)||(!client_secret)||(!refresh_token))
|
||||
throw {error:`Spotify token must contain client id/secret and refresh token`}
|
||||
throw {error:{message:`Spotify token must contain client id/secret and refresh token`}}
|
||||
//API call and parse tracklist
|
||||
try {
|
||||
//Request access token
|
||||
console.debug(`metrics/compute/${login}/plugins > music > requesting access token with refresh token for spotify`)
|
||||
console.debug(`metrics/compute/${login}/plugins > music > requesting access token with spotify refresh token`)
|
||||
const {data:{access_token:access}} = await imports.axios.post("https://accounts.spotify.com/api/token",
|
||||
`${new imports.url.URLSearchParams({grant_type:"refresh_token", refresh_token, client_id, client_secret})}`,
|
||||
{headers:{"Content-Type":"application/x-www-form-urlencoded"}},
|
||||
)
|
||||
console.debug(`metrics/compute/${login}/plugins > music > got new access token`)
|
||||
console.debug(`metrics/compute/${login}/plugins > music > got access token`)
|
||||
//Retriev tracks
|
||||
console.debug(`metrics/compute/${login}/plugins > music > querying spotify api`)
|
||||
tracks = (await imports.axios(`https://api.spotify.com/v1/me/player/recently-played?limit=${limit}&after=${timestamp}`, {headers:{
|
||||
"Accept":"application/json",
|
||||
"Content-Type":"application/json",
|
||||
@@ -143,8 +145,13 @@
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if ((error.response?.status))
|
||||
throw {error:{message:`API returned ${error.response.status}${error.response.data?.error_description ? ` (${error.response.data.error_description})` : ""}`}, ...raw}
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response.data?.error_description ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
throw {error:{message, instance:error}, ...raw}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
break
|
||||
@@ -173,7 +180,6 @@
|
||||
track.artwork = await imports.imgb64(track.artwork)
|
||||
}
|
||||
//Save results
|
||||
console.debug(`metrics/compute/${login}/plugins > music > success`)
|
||||
return {...raw, tracks}
|
||||
}
|
||||
//Unhandled error
|
||||
@@ -183,7 +189,6 @@
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
@@ -15,29 +15,37 @@
|
||||
url = `https://${url}`
|
||||
const result = {url, detailed, scores:[], metrics:{}}
|
||||
//Load scores from API
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > querying api for ${url}`)
|
||||
const scores = new Map()
|
||||
await Promise.all(["performance", "accessibility", "best-practices", "seo"].map(async category => {
|
||||
const {score, title} = (await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}&key=${token}`)).data.lighthouseResult.categories[category]
|
||||
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}`)
|
||||
console.debug(request.data)
|
||||
const {score, title} = request.data.lighthouseResult.categories[category]
|
||||
scores.set(category, {score, title})
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > ${category} audit performed`)
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
|
||||
}))
|
||||
result.scores = [scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]
|
||||
//Detailed metrics
|
||||
if (detailed)
|
||||
Object.assign(result.metrics, ...(await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?&url=${url}&key=${token}`)).data.lighthouseResult.audits.metrics.details.items)
|
||||
//Integrity check
|
||||
if (result.scores.filter(score => score).length < 4)
|
||||
throw {error:{message:"Incomplete PageSpeed results"}, url}
|
||||
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}`)
|
||||
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})`)
|
||||
}
|
||||
//Results
|
||||
return result
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.response?.status)
|
||||
throw {error:{message:`PageSpeed token error (code ${error.response.status})`}, url}
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.error?.message?.match(/Lighthouse returned error: (?<description>[A-Z_]+)/)?.groups?.description ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,12 @@
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(30, Number(limit)))
|
||||
//Retrieve posts
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > processing with source ${source}`)
|
||||
let posts = null
|
||||
switch (source) {
|
||||
//Dev.to
|
||||
case "dev.to":{
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > querying api`)
|
||||
posts = (await imports.axios.get(`https://dev.to/api/articles?username=${login}&state=fresh`)).data.map(({title, readable_publish_date:date}) => ({title, date}))
|
||||
break
|
||||
}
|
||||
@@ -37,7 +39,6 @@
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(100, Number(limit)))
|
||||
//Retrieve contribution calendar from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > querying api`)
|
||||
const {user:{projects}} = await graphql(`
|
||||
query Projects {
|
||||
user(login: "${login}") {
|
||||
@@ -31,6 +32,7 @@
|
||||
`
|
||||
)
|
||||
//Iterate through projects and format them
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > processing ${projects.nodes.length} projects`)
|
||||
const list = []
|
||||
for (const project of projects.nodes) {
|
||||
//Format date
|
||||
@@ -52,7 +54,9 @@
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
let message = "An error occured"
|
||||
if (error.errors?.map(({type}) => type)?.includes("INSUFFICIENT_SCOPES"))
|
||||
message = "Insufficient token rights"
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,11 @@
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(20, Number(limit)))
|
||||
//Start puppeteer and navigate to topics
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > searching starred topics`)
|
||||
let topics = []
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > starting browser`)
|
||||
const browser = await imports.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/compute/${login}/plugins > topics > loaded ${await browser.version()}`)
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
//Iterate through pages
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
@@ -32,17 +33,22 @@
|
||||
description:li.querySelector(".f5").innerText,
|
||||
icon:li.querySelector("img")?.src ?? null,
|
||||
})))
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > extracted ${starred.length} starred topics`)
|
||||
//Check if next page exists
|
||||
if (!starred.length)
|
||||
if (!starred.length) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > no more page to load`)
|
||||
break
|
||||
}
|
||||
topics.push(...starred)
|
||||
}
|
||||
//Close browser
|
||||
console.debug(`metrics/compute/${login}/plugins > music > closing browser`)
|
||||
await browser.close()
|
||||
//Shuffle topics
|
||||
if (shuffle)
|
||||
if (shuffle) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > shuffling topics`)
|
||||
topics = imports.shuffle(topics)
|
||||
}
|
||||
//Limit topics
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
|
||||
@@ -51,6 +57,7 @@
|
||||
topics.push({name:`And ${removed.length} more...`, description:removed.map(({name}) => name).join(", "), icon:null})
|
||||
}
|
||||
//Convert icons to base64
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > loading artworks`)
|
||||
for (const topic of topics) {
|
||||
if (topic.icon) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > processing ${topic.name}`)
|
||||
@@ -64,7 +71,6 @@
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
@@ -8,9 +8,11 @@
|
||||
//Repositories
|
||||
const repositories = data.user.repositories.nodes.map(({name}) => name) ?? []
|
||||
//Get views stats from repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > traffic > querying api`)
|
||||
const views = {count:0, uniques:0}
|
||||
const response = await Promise.all(repositories.map(async repo => await rest.repos.getViews({owner:login, repo})))
|
||||
//Compute views
|
||||
console.debug(`metrics/compute/${login}/plugins > traffic > computing stats`)
|
||||
response.filter(({data}) => data).map(({data:{count, uniques}}) => (views.count += count, views.uniques += uniques))
|
||||
//Format values
|
||||
views.count = imports.format(views.count)
|
||||
@@ -20,9 +22,9 @@
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.status === 403)
|
||||
throw {error:{message:`Insufficient token rights`}}
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
message = "Insufficient token rights"
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
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`)
|
||||
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}`}})
|
||||
//Load profile image
|
||||
if (profile?.profile_image_url) {
|
||||
@@ -50,7 +50,13 @@
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
throw {error:{message:`An error occured`}}
|
||||
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}}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//Imports
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import util from "util"
|
||||
|
||||
/** Setup */
|
||||
export default async function ({log = true} = {}) {
|
||||
@@ -30,7 +31,7 @@
|
||||
conf.settings.plugins = {}
|
||||
conf.settings.plugins.base = {parts:["header", "activity", "community", "repositories", "metadata"]}
|
||||
if (conf.settings.debug)
|
||||
logger(conf.settings)
|
||||
logger(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256}))
|
||||
|
||||
//Load package settings
|
||||
logger(`metrics/setup > load package.json`)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
+ (!!plugins.isocalendar)*192 + (plugins.isocalendar?.duration === 'full-year')*100
|
||||
+ (!!plugins.gists)*68
|
||||
+ (!!plugins.topics)*160
|
||||
+ (!!plugins.projects)*22 + (plugins.projects?.list?.length ?? 0)*60
|
||||
+ (!!plugins.projects)*22 + (plugins.projects?.list?.length ?? 0)*60 + (!!plugins.projects?.error)*22
|
||||
+ (!!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
|
||||
%>">
|
||||
@@ -774,7 +774,7 @@
|
||||
|
||||
<% if (base.metadata) { %>
|
||||
<footer>
|
||||
<span>These metrics <%= !computed.token.scopes.includes("repo") ? "does not include" : "includes" %> private contributions</span>
|
||||
<span>These metrics <%= !computed.token.scopes.includes("repo") ? "does not include all" : "includes" %> private contributions<% if ((config.timezone?.name)&&(!config.timezone?.error)) { %>, timezone <%= config.timezone.name %><% } %></span>
|
||||
<span>Last updated <%= new Date().toGMTString() %> with lowlighter/metrics@<%= meta.version %></span>
|
||||
</footer>
|
||||
<% } %>
|
||||
|
||||
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 73 KiB |
@@ -5,18 +5,36 @@
|
||||
const computed = data.computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_merged:0, forks:0, releases:0}}
|
||||
const avatar = imports.imgb64(data.user.avatarUrl)
|
||||
data.plugins = {}
|
||||
console.debug(`metrics/compute/${login} > formatting common metrics`)
|
||||
|
||||
//Timezone config
|
||||
if (q["config.timezone"]) {
|
||||
const timezone = data.config.timezone = {name:q["config.timezone"], offset:0}
|
||||
try {
|
||||
timezone.offset = Number(new Date().toLocaleString("fr", {timeZoneName:"short", timeZone:timezone.name}).match(/UTC[+](?<offset>\d+)/)?.groups?.offset*60*60*1000) || 0
|
||||
console.debug(`metrics/compute/${login} > timezone set to ${timezone.name} (${timezone.offset > 0 ? "+" : ""}${Math.round(timezone.offset/(60*60*1000))} hours)`)
|
||||
} catch {
|
||||
timezone.error = `Failed to use timezone "${timezone.name}"`
|
||||
console.debug(`metrics/compute/${login} > failed to use timezone "${timezone.name}"`)
|
||||
}
|
||||
}
|
||||
|
||||
//Plugins
|
||||
for (const name of Object.keys(imports.plugins)) {
|
||||
pending.push((async () => {
|
||||
try {
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
|
||||
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql}, plugins[name])
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > completed (${data.plugins[name] !== null ? "success" : "skipped"})`)
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > completed (error)`)
|
||||
data.plugins[name] = error
|
||||
}
|
||||
finally {
|
||||
return {name, result:data.plugins[name]}
|
||||
const result = {name, result:data.plugins[name]}
|
||||
console.debug(imports.util.inspect(result, {depth:Infinity, maxStringLength:256}))
|
||||
return result
|
||||
}
|
||||
})())
|
||||
}
|
||||
@@ -30,14 +48,14 @@
|
||||
computed.repositories.forks += repository.forkCount
|
||||
//License
|
||||
if (repository.licenseInfo)
|
||||
computed.licenses.used[repository.licenseInfo.spdxId] = (computed.licenses.used[repository.licenseInfo.spdxId] || 0) + 1
|
||||
computed.licenses.used[repository.licenseInfo.spdxId] = (computed.licenses.used[repository.licenseInfo.spdxId] ?? 0) + 1
|
||||
}
|
||||
|
||||
//Total disk usage
|
||||
computed.diskUsage = `${imports.bytes(data.user.repositories.totalDiskUsage*1000)}`
|
||||
|
||||
//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) || ""
|
||||
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
|
||||
computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
|
||||
@@ -62,11 +80,11 @@
|
||||
data.meta = {version:conf.package.version, author:conf.package.author}
|
||||
|
||||
//Debug flags
|
||||
if (dflags.includes("--cakeday")||q["dflag.cakeday"]) {
|
||||
if ((dflags.includes("--cakeday"))||(q["dflag.cakeday"])) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --cakeday`)
|
||||
computed.cakeday = true
|
||||
}
|
||||
if (dflags.includes("--hireable")||q["dflag.hireable"]) {
|
||||
if ((dflags.includes("--hireable"))||(q["dflag.hireable"])) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --hireable`)
|
||||
data.user.isHireable = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user