chore: code formatting

This commit is contained in:
github-actions[bot]
2022-04-23 23:18:43 +00:00
parent 73cd43c18f
commit 4c98629bbc
130 changed files with 1839 additions and 1788 deletions

View File

@@ -1,7 +1,7 @@
//Imports //Imports
import fs from "fs/promises"
import ejs from "ejs" import ejs from "ejs"
import fss from "fs" import fss from "fs"
import fs from "fs/promises"
import yaml from "js-yaml" import yaml from "js-yaml"
import paths from "path" import paths from "path"
import sgit from "simple-git" import sgit from "simple-git"
@@ -26,13 +26,13 @@ const __test_secrets = paths.join(paths.join(__metrics, "tests/secrets.json"))
//Git setup //Git setup
const git = sgit(__metrics) const git = sgit(__metrics)
const staged = new Set() const staged = new Set()
const secrets = Object.assign(JSON.parse(`${await fs.readFile(__test_secrets)}`), { $regex: /\$\{\{\s*secrets\.(?<secret>\w+)\s*\}\}/ }) const secrets = Object.assign(JSON.parse(`${await fs.readFile(__test_secrets)}`), {$regex: /\$\{\{\s*secrets\.(?<secret>\w+)\s*\}\}/})
const { plugins, templates } = await metadata({ log: false, diff: true }) const {plugins, templates} = await metadata({log: false, diff: true})
const workflow = [] const workflow = []
//Plugins //Plugins
for (const id of Object.keys(plugins)) { for (const id of Object.keys(plugins)) {
const { examples, options, readme, tests, header, community } = await plugin(id) const {examples, options, readme, tests, header, community} = await plugin(id)
//Readme //Readme
console.log(`Generating source/plugins/${community ? "community/" : ""}${id}/README.md`) console.log(`Generating source/plugins/${community ? "community/" : ""}${id}/README.md`)
@@ -40,7 +40,7 @@ for (const id of Object.keys(plugins)) {
readme.path, readme.path,
readme.content readme.content
.replace(/(<!--header-->)[\s\S]*(<!--\/header-->)/g, `$1\n${header}\n$2`) .replace(/(<!--header-->)[\s\S]*(<!--\/header-->)/g, `$1\n${header}\n$2`)
.replace(/(<!--examples-->)[\s\S]*(<!--\/examples-->)/g, `$1\n${examples.map(({ test, prod, ...step }) => ["```yaml", yaml.dump(step, { quotingType: '"', noCompatMode: true }), "```"].join("\n")).join("\n")}\n$2`) .replace(/(<!--examples-->)[\s\S]*(<!--\/examples-->)/g, `$1\n${examples.map(({test, prod, ...step}) => ["```yaml", yaml.dump(step, {quotingType: '"', noCompatMode: true}), "```"].join("\n")).join("\n")}\n$2`)
.replace(/(<!--options-->)[\s\S]*(<!--\/options-->)/g, `$1\n${options}\n$2`), .replace(/(<!--options-->)[\s\S]*(<!--\/options-->)/g, `$1\n${options}\n$2`),
) )
staged.add(readme.path) staged.add(readme.path)
@@ -54,7 +54,7 @@ for (const id of Object.keys(plugins)) {
//Templates //Templates
for (const id of Object.keys(templates)) { for (const id of Object.keys(templates)) {
const { examples, readme, tests, header } = await template(id) const {examples, readme, tests, header} = await template(id)
//Readme //Readme
console.log(`Generating source/templates/${id}/README.md`) console.log(`Generating source/templates/${id}/README.md`)
@@ -62,14 +62,14 @@ for (const id of Object.keys(templates)) {
readme.path, readme.path,
readme.content readme.content
.replace(/(<!--header-->)[\s\S]*(<!--\/header-->)/g, `$1\n${header}\n$2`) .replace(/(<!--header-->)[\s\S]*(<!--\/header-->)/g, `$1\n${header}\n$2`)
.replace(/(<!--examples-->)[\s\S]*(<!--\/examples-->)/g, `$1\n${examples.map(({ test, prod, ...step }) => ["```yaml", yaml.dump(step, { quotingType: '"', noCompatMode: true }), "```"].join("\n")).join("\n")}\n$2`), .replace(/(<!--examples-->)[\s\S]*(<!--\/examples-->)/g, `$1\n${examples.map(({test, prod, ...step}) => ["```yaml", yaml.dump(step, {quotingType: '"', noCompatMode: true}), "```"].join("\n")).join("\n")}\n$2`),
) )
staged.add(readme.path) staged.add(readme.path)
//Tests //Tests
console.log(`Generating tests/templates/${id}.yml`) console.log(`Generating tests/templates/${id}.yml`)
workflow.push(...examples.map(example => testcase(templates[id].name, "prod", example)).filter(t => t)) workflow.push(...examples.map(example => testcase(templates[id].name, "prod", example)).filter(t => t))
await fs.writeFile(tests.path, yaml.dump(examples.map(example => testcase(templates[id].name, "test", example)).filter(t => t), { quotingType: '"', noCompatMode: true })) await fs.writeFile(tests.path, yaml.dump(examples.map(example => testcase(templates[id].name, "test", example)).filter(t => t), {quotingType: '"', noCompatMode: true}))
staged.add(tests.path) staged.add(tests.path)
} }
@@ -77,21 +77,21 @@ for (const id of Object.keys(templates)) {
for (const step of ["config", "documentation"]) { for (const step of ["config", "documentation"]) {
switch (step) { switch (step) {
case "config": case "config":
await update({ source: paths.join(__action, "action.yml"), output: "action.yml" }) await update({source: paths.join(__action, "action.yml"), output: "action.yml"})
await update({ source: paths.join(__web, "settings.example.json"), output: "settings.example.json" }) await update({source: paths.join(__web, "settings.example.json"), output: "settings.example.json"})
break break
case "documentation": case "documentation":
await update({ source: paths.join(__documentation, "README.md"), output: "README.md", options: { root: __readme } }) await update({source: paths.join(__documentation, "README.md"), output: "README.md", options: {root: __readme}})
await update({ source: paths.join(__documentation, "plugins.md"), output: "source/plugins/README.md" }) await update({source: paths.join(__documentation, "plugins.md"), output: "source/plugins/README.md"})
await update({ source: paths.join(__documentation, "plugins.community.md"), output: "source/plugins/community/README.md" }) await update({source: paths.join(__documentation, "plugins.community.md"), output: "source/plugins/community/README.md"})
await update({ source: paths.join(__documentation, "templates.md"), output: "source/templates/README.md" }) await update({source: paths.join(__documentation, "templates.md"), output: "source/templates/README.md"})
await update({ source: paths.join(__documentation, "compatibility.md"), output: ".github/readme/partials/documentation/compatibility.md" }) await update({source: paths.join(__documentation, "compatibility.md"), output: ".github/readme/partials/documentation/compatibility.md"})
break break
} }
} }
//Example workflows //Example workflows
await update({ source: paths.join(__metrics, ".github/scripts/files/examples.yml"), output: ".github/workflows/examples.yml", context: { steps: yaml.dump(workflow, { quotingType: '"', noCompatMode: true }) } }) await update({source: paths.join(__metrics, ".github/scripts/files/examples.yml"), output: ".github/workflows/examples.yml", context: {steps: yaml.dump(workflow, {quotingType: '"', noCompatMode: true})}})
//Commit and push //Commit and push
if (mode === "publish") { if (mode === "publish") {
@@ -109,10 +109,10 @@ console.log("Success!")
//================================================================================== //==================================================================================
//Update generated files //Update generated files
async function update({ source, output, context = {}, options = {} }) { async function update({source, output, context = {}, options = {}}) {
console.log(`Generating ${output}`) console.log(`Generating ${output}`)
const { plugins, templates, packaged, descriptor } = await metadata({ log: false }) const {plugins, templates, packaged, descriptor} = await metadata({log: false})
const content = await ejs.renderFile(source, { plugins, templates, packaged, descriptor, ...context }, { async: true, ...options }) const content = await ejs.renderFile(source, {plugins, templates, packaged, descriptor, ...context}, {async: true, ...options})
const file = paths.join(__metrics, output) const file = paths.join(__metrics, output)
await fs.writeFile(file, content) await fs.writeFile(file, content)
staged.add(file) staged.add(file)
@@ -160,15 +160,15 @@ async function template(id) {
//Testcase generator //Testcase generator
function testcase(name, env, args) { function testcase(name, env, args) {
const { prod = {}, test = {}, ...step } = JSON.parse(JSON.stringify(args)) const {prod = {}, test = {}, ...step} = JSON.parse(JSON.stringify(args))
const context = { prod, test }[env] ?? {} const context = {prod, test}[env] ?? {}
const { with: overrides } = context const {with: overrides} = context
if (context.skip) if (context.skip)
return null return null
Object.assign(step.with, context.with ?? {}) Object.assign(step.with, context.with ?? {})
delete context.with delete context.with
const result = { ...step, ...context, name: `${name} - ${step.name ?? "(unnamed)"}` } const result = {...step, ...context, name: `${name} - ${step.name ?? "(unnamed)"}`}
for (const [k, v] of Object.entries(result.with)) { for (const [k, v] of Object.entries(result.with)) {
if ((env === "test") && (secrets.$regex.test(v))) if ((env === "test") && (secrets.$regex.test(v)))
result.with[k] = v.replace(secrets.$regex, secrets[v.match(secrets.$regex)?.groups?.secret]) result.with[k] = v.replace(secrets.$regex, secrets[v.match(secrets.$regex)?.groups?.secret])
@@ -177,21 +177,21 @@ function testcase(name, env, args) {
if (env === "prod") { if (env === "prod") {
result.if = "${{ success() || failure() }}" result.if = "${{ success() || failure() }}"
result.uses = "lowlighter/metrics@master" result.uses = "lowlighter/metrics@master"
Object.assign(result.with, { output_action: "none", delay: 120 }) Object.assign(result.with, {output_action: "none", delay: 120})
for (const { property, value } of [{ property: "user", value: "lowlighter" }, { property: "plugins_errors_fatal", value: "yes" }]) { for (const {property, value} of [{property: "user", value: "lowlighter"}, {property: "plugins_errors_fatal", value: "yes"}]) {
if (!(property in result.with)) if (!(property in result.with))
result.with[property] = value result.with[property] = value
} }
if ((overrides?.output_action) && (overrides?.committer_branch === "examples")) if ((overrides?.output_action) && (overrides?.committer_branch === "examples"))
Object.assign(result.with, { output_action: overrides.output_action, committer_branch: "examples" }) Object.assign(result.with, {output_action: overrides.output_action, committer_branch: "examples"})
} }
if (env === "test") { if (env === "test") {
if (!result.with.base) if (!result.with.base)
delete result.with.base delete result.with.base
delete result.with.filename delete result.with.filename
Object.assign(result.with, { use_mocked_data: "yes", verify: "yes" }) Object.assign(result.with, {use_mocked_data: "yes", verify: "yes"})
} }
return result return result

View File

@@ -11,11 +11,11 @@ const browser = await puppeteer.launch({
const page = await browser.newPage() const page = await browser.newPage()
//Select markdown example and take screenshoot //Select markdown example and take screenshoot
await page.setViewport({ width: 600, height: 600 }) await page.setViewport({width: 600, height: 600})
await page.goto("https://github.com/lowlighter/metrics/blob/examples/metrics.markdown.md") await page.goto("https://github.com/lowlighter/metrics/blob/examples/metrics.markdown.md")
const clip = await page.evaluate(() => { const clip = await page.evaluate(() => {
const { x, y, width, height } = document.querySelector("#readme").getBoundingClientRect() const {x, y, width, height} = document.querySelector("#readme").getBoundingClientRect()
return { x, y, width, height } return {x, y, width, height}
}) })
await page.screenshot({ type: "png", path: "/tmp/metrics.markdown.png", clip, omitBackground: true }) await page.screenshot({type: "png", path: "/tmp/metrics.markdown.png", clip, omitBackground: true})
await browser.close() await browser.close()

View File

@@ -1,6 +1,6 @@
//Imports //Imports
import fs from "fs/promises"
import processes from "child_process" import processes from "child_process"
import fs from "fs/promises"
import yaml from "js-yaml" import yaml from "js-yaml"
import fetch from "node-fetch" import fetch from "node-fetch"
import paths from "path" import paths from "path"
@@ -16,7 +16,7 @@ const __metrics = paths.join(paths.dirname(url.fileURLToPath(import.meta.url)),
const __presets = paths.join(__metrics, ".presets") const __presets = paths.join(__metrics, ".presets")
if ((!await fs.access(__presets).then(_ => true).catch(_ => false)) || (!(await fs.lstat(__presets)).isDirectory())) if ((!await fs.access(__presets).then(_ => true).catch(_ => false)) || (!(await fs.lstat(__presets)).isDirectory()))
await sgit().clone(`https://github-actions[bot]:${process.env.GITHUB_TOKEN}@github.com/lowlighter/metrics`, __presets, { "--branch": "presets", "--single-branch": true }) await sgit().clone(`https://github-actions[bot]:${process.env.GITHUB_TOKEN}@github.com/lowlighter/metrics`, __presets, {"--branch": "presets", "--single-branch": true})
const git = sgit(__presets) const git = sgit(__presets)
await git.pull() await git.pull()
const staged = new Set() const staged = new Set()
@@ -27,7 +27,7 @@ web.run = async vars => await fetch(`http://localhost:3000/lowlighter?${new url.
web.start = async () => web.start = async () =>
new Promise(solve => { new Promise(solve => {
let stdout = "" let stdout = ""
web.instance = processes.spawn("node", ["source/app/web/index.mjs"], { env: { ...process.env, SANDBOX: true } }) web.instance = processes.spawn("node", ["source/app/web/index.mjs"], {env: {...process.env, SANDBOX: true}})
web.instance.stdout.on("data", data => (stdout += data, /Server ready !/.test(stdout) ? solve() : null)) web.instance.stdout.on("data", data => (stdout += data, /Server ready !/.test(stdout) ? solve() : null))
web.instance.stderr.on("data", data => console.error(`${data}`)) web.instance.stderr.on("data", data => console.error(`${data}`))
}) })
@@ -44,13 +44,13 @@ for (const path of await fs.readdir(__presets)) {
//Example //Example
console.log(`generating: ${preset}/example.svg`) console.log(`generating: ${preset}/example.svg`)
const svg = await web.run({ config_presets: `@${preset}`, plugins_errors_fatal: true }) const svg = await web.run({config_presets: `@${preset}`, plugins_errors_fatal: true})
await fs.writeFile(paths.join(__presets, path, "example.svg"), svg) await fs.writeFile(paths.join(__presets, path, "example.svg"), svg)
staged.add(paths.join(__presets, path, "example.svg")) staged.add(paths.join(__presets, path, "example.svg"))
//Readme //Readme
console.log(`generating: ${preset}/README.svg`) console.log(`generating: ${preset}/README.svg`)
const { name, description } = await yaml.load(await fs.readFile(paths.join(__presets, preset, "preset.yml"))) const {name, description} = await yaml.load(await fs.readFile(paths.join(__presets, preset, "preset.yml")))
await fs.writeFile( await fs.writeFile(
paths.join(__presets, path, "README.md"), paths.join(__presets, path, "README.md"),
` `

View File

@@ -19,22 +19,22 @@ const __preview_templates_ = paths.join(__preview, ".templates_")
const __preview_about = paths.join(__preview, "about/.statics") const __preview_about = paths.join(__preview, "about/.statics")
//Extract from web server //Extract from web server
const { conf, Templates } = await setup({ log: false }) const {conf, Templates} = await setup({log: false})
const templates = Object.entries(Templates).map(([name]) => ({ name, enabled: true })) const templates = Object.entries(Templates).map(([name]) => ({name, enabled: true}))
const metadata = Object.fromEntries( const metadata = Object.fromEntries(
Object.entries(conf.metadata.plugins) Object.entries(conf.metadata.plugins)
.map(([key, value]) => [key, Object.fromEntries(Object.entries(value).filter(([key]) => ["name", "icon", "category", "web", "supports", "scopes"].includes(key)))]) .map(([key, value]) => [key, Object.fromEntries(Object.entries(value).filter(([key]) => ["name", "icon", "category", "web", "supports", "scopes"].includes(key)))])
.map(([key, value]) => [key, key === "core" ? { ...value, web: Object.fromEntries(Object.entries(value.web).filter(([key]) => /^config[.]/.test(key)).map(([key, value]) => [key.replace(/^config[.]/, ""), value])) } : value]), .map(([key, value]) => [key, key === "core" ? {...value, web: Object.fromEntries(Object.entries(value.web).filter(([key]) => /^config[.]/.test(key)).map(([key, value]) => [key.replace(/^config[.]/, ""), value]))} : value]),
) )
const enabled = Object.entries(metadata).filter(([_name, { category }]) => category !== "core").map(([name]) => ({ name, category: metadata[name]?.category ?? "community", enabled: true })) const enabled = Object.entries(metadata).filter(([_name, {category}]) => category !== "core").map(([name]) => ({name, category: metadata[name]?.category ?? "community", enabled: true}))
//Directories //Directories
await fs.mkdir(__preview, { recursive: true }) await fs.mkdir(__preview, {recursive: true})
await fs.mkdir(__preview_js, { recursive: true }) await fs.mkdir(__preview_js, {recursive: true})
await fs.mkdir(__preview_css, { recursive: true }) await fs.mkdir(__preview_css, {recursive: true})
await fs.mkdir(__preview_templates, { recursive: true }) await fs.mkdir(__preview_templates, {recursive: true})
await fs.mkdir(__preview_templates_, { recursive: true }) await fs.mkdir(__preview_templates_, {recursive: true})
await fs.mkdir(__preview_about, { recursive: true }) await fs.mkdir(__preview_about, {recursive: true})
//Web //Web
fs.copyFile(paths.join(__web, "index.html"), paths.join(__preview, "index.html")) fs.copyFile(paths.join(__web, "index.html"), paths.join(__preview, "index.html"))
@@ -49,7 +49,7 @@ for (const template in conf.templates) {
fs.writeFile(paths.join(__preview_templates_, template), JSON.stringify(conf.templates[template])) fs.writeFile(paths.join(__preview_templates_, template), JSON.stringify(conf.templates[template]))
const __partials = paths.join(__templates, template, "partials") const __partials = paths.join(__templates, template, "partials")
const __preview_partials = paths.join(__preview_templates, template, "partials") const __preview_partials = paths.join(__preview_templates, template, "partials")
await fs.mkdir(__preview_partials, { recursive: true }) await fs.mkdir(__preview_partials, {recursive: true})
for (const file of await fs.readdir(__partials)) for (const file of await fs.readdir(__partials))
fs.copyFile(paths.join(__partials, file), paths.join(__preview_partials, file)) fs.copyFile(paths.join(__partials, file), paths.join(__preview_partials, file))
} }
@@ -73,7 +73,7 @@ fs.copyFile(paths.join(__node_modules, "prismjs/components/prism-markdown.min.js
fs.copyFile(paths.join(__node_modules, "clipboard/dist/clipboard.min.js"), paths.join(__preview_js, "clipboard.min.js")) fs.copyFile(paths.join(__node_modules, "clipboard/dist/clipboard.min.js"), paths.join(__preview_js, "clipboard.min.js"))
//Meta //Meta
fs.writeFile(paths.join(__preview, ".version"), JSON.stringify(`${conf.package.version}-preview`)) fs.writeFile(paths.join(__preview, ".version"), JSON.stringify(`${conf.package.version}-preview`))
fs.writeFile(paths.join(__preview, ".hosted"), JSON.stringify({ by: "metrics", link: "https://github.com/lowlighter/metrics" })) fs.writeFile(paths.join(__preview, ".hosted"), JSON.stringify({by: "metrics", link: "https://github.com/lowlighter/metrics"}))
//About //About
fs.copyFile(paths.join(__web, "about", "index.html"), paths.join(__preview, "about", "index.html")) fs.copyFile(paths.join(__web, "about", "index.html"), paths.join(__preview, "about", "index.html"))
for (const file of await fs.readdir(__web_about)) { for (const file of await fs.readdir(__web_about)) {

View File

@@ -26,7 +26,7 @@ if (!version)
console.log(`Version: ${version}`) console.log(`Version: ${version}`)
//Load related pr //Load related pr
const { data: { items: prs } } = await rest.search.issuesAndPullRequests({ const {data: {items: prs}} = await rest.search.issuesAndPullRequests({
q: `repo:${repository.owner}/${repository.name} is:pr is:merged author:${maintainer} assignee:${maintainer} Release ${version} in:title`, q: `repo:${repository.owner}/${repository.name} is:pr is:merged author:${maintainer} assignee:${maintainer} Release ${version} in:title`,
}) })
@@ -40,9 +40,9 @@ console.log(`Using pr#${patchnote.number}: ${patchnote.title}`)
//Check whether release already exists //Check whether release already exists
try { try {
const { data: { id } } = await rest.repos.getReleaseByTag({ owner: repository.owner, repo: repository.name, tag: version }) const {data: {id}} = await rest.repos.getReleaseByTag({owner: repository.owner, repo: repository.name, tag: version})
console.log(`Release ${version} already exists (#${id}), will replace it`) console.log(`Release ${version} already exists (#${id}), will replace it`)
await rest.repos.deleteRelease({ owner: repository.owner, repo: repository.name, release_id: id }) await rest.repos.deleteRelease({owner: repository.owner, repo: repository.name, release_id: id})
console.log(`Deleting tag ${version}`) console.log(`Deleting tag ${version}`)
await git.push(["--delete", "origin", version]) await git.push(["--delete", "origin", version])
await new Promise(solve => setTimeout(solve, 15 * 1000)) await new Promise(solve => setTimeout(solve, 15 * 1000))
@@ -52,5 +52,5 @@ catch {
} }
//Publish release //Publish release
await rest.repos.createRelease({ owner: repository.owner, repo: repository.name, tag_name: version, name: `Version ${version.replace(/^v/g, "")}`, body: patchnote.body }) await rest.repos.createRelease({owner: repository.owner, repo: repository.name, tag_name: version, name: `Version ${version.replace(/^v/g, "")}`, body: patchnote.body})
console.log(`Successfully published`) console.log(`Successfully published`)

View File

@@ -2,8 +2,8 @@
import core from "@actions/core" import core from "@actions/core"
import github from "@actions/github" import github from "@actions/github"
import octokit from "@octokit/graphql" import octokit from "@octokit/graphql"
import fs from "fs/promises"
import processes from "child_process" import processes from "child_process"
import fs from "fs/promises"
import paths from "path" import paths from "path"
import sgit from "simple-git" import sgit from "simple-git"
import mocks from "../../../tests/mocks/index.mjs" import mocks from "../../../tests/mocks/index.mjs"
@@ -22,7 +22,8 @@ const debugged = []
const preset = {} const preset = {}
//Info logger //Info logger
const info = (left, right, {token = false} = {}) => console.log(`${`${left}`.padEnd(63 + 9 * (/0m$/.test(left)))}${ const info = (left, right, {token = false} = {}) =>
console.log(`${`${left}`.padEnd(63 + 9 * (/0m$/.test(left)))}${
Array.isArray(right) Array.isArray(right)
? right.join(", ") || "(none)" ? right.join(", ") || "(none)"
: right === undefined : right === undefined
@@ -37,7 +38,7 @@ info.section = (left = "", right = " ") => info(`\x1b[36m${left}\x1b[0m`, right)
info.group = ({metadata, name, inputs}) => { info.group = ({metadata, name, inputs}) => {
info.section(metadata.plugins[name]?.name?.match(/(?<section>[\w\s]+)/i)?.groups?.section?.trim(), " ") info.section(metadata.plugins[name]?.name?.match(/(?<section>[\w\s]+)/i)?.groups?.section?.trim(), " ")
for (const [input, value] of Object.entries(inputs)) for (const [input, value] of Object.entries(inputs))
info(metadata.plugins[name]?.inputs[input]?.description?.split("\n")[0] ?? metadata.plugins[name]?.inputs[input]?.description ?? input, `${input in preset ? "*" : ""}${value}`, {token:metadata.plugins[name]?.inputs[input]?.type === "token"}) info(metadata.plugins[name]?.inputs[input]?.description?.split("\n")[0] ?? metadata.plugins[name]?.inputs[input]?.description ?? input, `${input in preset ? "*" : ""}${value}`, {token: metadata.plugins[name]?.inputs[input]?.type === "token"})
} }
info.break = () => console.log("─".repeat(88)) info.break = () => console.log("─".repeat(88))
@@ -70,14 +71,12 @@ async function retry(func, {retries = 1, delay = 0} = {}) {
//Process exit //Process exit
function quit(reason) { function quit(reason) {
const code = {success:0, skipped:0, failed:1}[reason] ?? 0 const code = {success: 0, skipped: 0, failed: 1}[reason] ?? 0
process.exit(code) process.exit(code)
} } //=====================================================================================================
//=====================================================================================================
//Runner //Runner
(async function() {
;(async function() {
try { try {
//Initialization //Initialization
info.break() info.break()
@@ -96,9 +95,9 @@ function quit(reason) {
} }
//Load configuration //Load configuration
const {conf, Plugins, Templates} = await setup({log:false, community:{templates:core.getInput("setup_community_templates")}}) const {conf, Plugins, Templates} = await setup({log: false, community: {templates: core.getInput("setup_community_templates")}})
const {metadata} = conf const {metadata} = conf
conf.settings.extras = {default:true} conf.settings.extras = {default: true}
info("Setup", "complete") info("Setup", "complete")
info("Version", conf.package.version) info("Version", conf.package.version)
@@ -111,42 +110,42 @@ function quit(reason) {
} }
//Core inputs //Core inputs
Object.assign(preset, await presets(core.getInput("config_presets"), {log:false, core})) Object.assign(preset, await presets(core.getInput("config_presets"), {log: false, core}))
const { const {
user:_user, user: _user,
repo:_repo, repo: _repo,
token, token,
template, template,
query, query,
"setup.community.templates":_templates, "setup.community.templates": _templates,
filename:_filename, filename: _filename,
optimize, optimize,
verify, verify,
"markdown.cache":_markdown_cache, "markdown.cache": _markdown_cache,
debug, debug,
"debug.flags":dflags, "debug.flags": dflags,
"debug.print":dprint, "debug.print": dprint,
"use.mocked.data":mocked, "use.mocked.data": mocked,
dryrun, dryrun,
"plugins.errors.fatal":die, "plugins.errors.fatal": die,
"committer.token":_token, "committer.token": _token,
"committer.branch":_branch, "committer.branch": _branch,
"committer.message":_message, "committer.message": _message,
"committer.gist":_gist, "committer.gist": _gist,
"use.prebuilt.image":_image, "use.prebuilt.image": _image,
retries, retries,
"retries.delay":retries_delay, "retries.delay": retries_delay,
"retries.output.action":retries_output_action, "retries.output.action": retries_output_action,
"retries.delay.output.action":retries_delay_output_action, "retries.delay.output.action": retries_delay_output_action,
"output.action":_action, "output.action": _action,
"output.condition":_output_condition, "output.condition": _output_condition,
delay, delay,
"notice.release":_notice_releases, "notice.release": _notice_releases,
...config ...config
} = metadata.plugins.core.inputs.action({core, preset}) } = metadata.plugins.core.inputs.action({core, preset})
const q = {...query, ...(_repo ? {repo:_repo} : null), template} const q = {...query, ...(_repo ? {repo: _repo} : null), template}
const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(config["config.output"]) ? config["config.output"] : metadata.templates[template]?.formats?.[0] ?? null const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(config["config.output"]) ? config["config.output"] : metadata.templates[template]?.formats?.[0] ?? null
const filename = _filename.replace(/[*]/g, {jpeg:"jpg", markdown:"md", "markdown-pdf":"pdf", insights:"html"}[_output] ?? _output ?? "*") const filename = _filename.replace(/[*]/g, {jpeg: "jpg", markdown: "md", "markdown-pdf": "pdf", insights: "html"}[_output] ?? _output ?? "*")
//Docker image //Docker image
if (_image) if (_image)
@@ -162,7 +161,7 @@ function quit(reason) {
q["debug.flags"] = dflags.join(" ") q["debug.flags"] = dflags.join(" ")
//Token for data gathering //Token for data gathering
info("GitHub token", token, {token:true}) info("GitHub token", token, {token: true})
//A GitHub token should start with "gh" along an additional letter for type //A GitHub token should start with "gh" along an additional letter for type
//See https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats //See https://github.blog/2021-04-05-behind-githubs-new-authentication-token-formats
info("GitHub token format", /^gh[pousr]_/.test(token) ? "correct" : "(old or invalid)") info("GitHub token format", /^gh[pousr]_/.test(token) ? "correct" : "(old or invalid)")
@@ -171,7 +170,7 @@ function quit(reason) {
conf.settings.token = token conf.settings.token = token
const api = {} const api = {}
const resources = {} const resources = {}
api.graphql = octokit.graphql.defaults({headers:{authorization:`token ${token}`}}) api.graphql = octokit.graphql.defaults({headers: {authorization: `token ${token}`}})
info("Github GraphQL API", "ok") info("Github GraphQL API", "ok")
const octoraw = github.getOctokit(token) const octoraw = github.getOctokit(token)
api.rest = octoraw.rest api.rest = octoraw.rest
@@ -185,12 +184,12 @@ function quit(reason) {
//Test token validity and requests count //Test token validity and requests count
else if (!/^NOT_NEEDED$/.test(token)) { else if (!/^NOT_NEEDED$/.test(token)) {
//Check rate limit //Check rate limit
const {data} = await api.rest.rateLimit.get().catch(() => ({data:{resources:{}}})) const {data} = await api.rest.rateLimit.get().catch(() => ({data: {resources: {}}}))
Object.assign(resources, data.resources) Object.assign(resources, data.resources)
info("API requests (REST)", resources.core ? `${resources.core.remaining}/${resources.core.limit}` : "(unknown)") info("API requests (REST)", resources.core ? `${resources.core.remaining}/${resources.core.limit}` : "(unknown)")
info("API requests (GraphQL)", resources.graphql ? `${resources.graphql.remaining}/${resources.graphql.limit}` : "(unknown)") info("API requests (GraphQL)", resources.graphql ? `${resources.graphql.remaining}/${resources.graphql.limit}` : "(unknown)")
info("API requests (search)", resources.search ? `${resources.search.remaining}/${resources.search.limit}` : "(unknown)") info("API requests (search)", resources.search ? `${resources.search.remaining}/${resources.search.limit}` : "(unknown)")
if ((!resources.core.remaining)||(!resources.graphql.remaining)) { if ((!resources.core.remaining) || (!resources.graphql.remaining)) {
console.warn("::warning::It seems you have reached your API requests limit. Please retry later.") console.warn("::warning::It seems you have reached your API requests limit. Please retry later.")
info.break() info.break()
console.log("Nothing can be done currently, thanks for using metrics!") console.log("Nothing can be done currently, thanks for using metrics!")
@@ -217,7 +216,7 @@ function quit(reason) {
//Check for new versions //Check for new versions
if (_notice_releases) { if (_notice_releases) {
const {data:[{tag_name:tag}]} = await rest.repos.listReleases({owner:"lowlighter", repo:"metrics"}) const {data: [{tag_name: tag}]} = await rest.repos.listReleases({owner: "lowlighter", repo: "metrics"})
const current = Number(conf.package.version.match(/(\d+\.\d+)/)?.[1] ?? 0) const current = Number(conf.package.version.match(/(\d+\.\d+)/)?.[1] ?? 0)
const latest = Number(tag.match(/(\d+\.\d+)/)?.[1] ?? 0) const latest = Number(tag.match(/(\d+\.\d+)/)?.[1] ?? 0)
if (latest > current) if (latest > current)
@@ -254,7 +253,7 @@ function quit(reason) {
committer.merge = _action.match(/^pull-request-(?<method>merge|squash|rebase)$/)?.groups?.method ?? null committer.merge = _action.match(/^pull-request-(?<method>merge|squash|rebase)$/)?.groups?.method ?? null
committer.branch = _branch || github.context.ref.replace(/^refs[/]heads[/]/, "") committer.branch = _branch || github.context.ref.replace(/^refs[/]heads[/]/, "")
committer.head = committer.pr ? `metrics-run-${github.context.runId}` : committer.branch committer.head = committer.pr ? `metrics-run-${github.context.runId}` : committer.branch
info("Committer token", committer.token, {token:true}) info("Committer token", committer.token, {token: true})
if (!committer.token) if (!committer.token)
throw new Error("You must provide a valid GitHub token to commit your metrics") throw new Error("You must provide a valid GitHub token to commit your metrics")
info("Committer branch", committer.branch) info("Committer branch", committer.branch)
@@ -273,25 +272,25 @@ function quit(reason) {
} }
//Create head branch if needed //Create head branch if needed
try { try {
await committer.rest.git.getRef({...github.context.repo, ref:`heads/${committer.head}`}) await committer.rest.git.getRef({...github.context.repo, ref: `heads/${committer.head}`})
info("Committer head branch status", "ok") info("Committer head branch status", "ok")
} }
catch (error) { catch (error) {
console.debug(error) console.debug(error)
if (/not found/i.test(`${error}`)) { if (/not found/i.test(`${error}`)) {
const {data:{object:{sha}}} = await committer.rest.git.getRef({...github.context.repo, ref:`heads/${committer.branch}`}) const {data: {object: {sha}}} = await committer.rest.git.getRef({...github.context.repo, ref: `heads/${committer.branch}`})
info("Committer branch current sha", sha) info("Committer branch current sha", sha)
await committer.rest.git.createRef({...github.context.repo, ref:`refs/heads/${committer.head}`, sha}) await committer.rest.git.createRef({...github.context.repo, ref: `refs/heads/${committer.head}`, sha})
info("Committer head branch status", "(created)") info("Committer head branch status", "(created)")
} }
else else {
throw error throw error
}
} }
//Retrieve previous render SHA to be able to update file content through API //Retrieve previous render SHA to be able to update file content through API
committer.sha = null committer.sha = null
try { try {
const {repository:{object:{oid}}} = await graphql( const {repository: {object: {oid}}} = await graphql(
` `
query Sha { query Sha {
repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") { repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
@@ -299,7 +298,7 @@ function quit(reason) {
} }
} }
`, `,
{headers:{authorization:`token ${committer.token}`}}, {headers: {authorization: `token ${committer.token}`}},
) )
committer.sha = oid committer.sha = oid
} }
@@ -308,8 +307,9 @@ function quit(reason) {
} }
info("Previous render sha", committer.sha ?? "(none)") info("Previous render sha", committer.sha ?? "(none)")
} }
else if (dryrun) else if (dryrun) {
info("Dry-run", true) info("Dry-run", true)
}
//SVG file //SVG file
conf.settings.optimize = optimize conf.settings.optimize = optimize
@@ -326,7 +326,7 @@ function quit(reason) {
//Core config //Core config
info.break() info.break()
info.group({metadata, name:"core", inputs:config}) info.group({metadata, name: "core", inputs: config})
info("Plugin errors", die ? "(exit with error)" : "(displayed in generated image)") info("Plugin errors", die ? "(exit with error)" : "(displayed in generated image)")
const convert = _output || null const convert = _output || null
Object.assign(q, config) Object.assign(q, config)
@@ -337,7 +337,7 @@ function quit(reason) {
await new Promise(async (solve, reject) => { await new Promise(async (solve, reject) => {
let stdout = "" let stdout = ""
setTimeout(() => reject("Timeout while waiting for Insights webserver"), 5 * 60 * 1000) setTimeout(() => reject("Timeout while waiting for Insights webserver"), 5 * 60 * 1000)
const web = await processes.spawn("node", ["/metrics/source/app/web/index.mjs"], {env:{...process.env}}) const web = await processes.spawn("node", ["/metrics/source/app/web/index.mjs"], {env: {...process.env}})
web.stdout.on("data", data => (console.debug(`web > ${data}`), stdout += data, /Server ready !/.test(stdout) ? solve() : null)) web.stdout.on("data", data => (console.debug(`web > ${data}`), stdout += data, /Server ready !/.test(stdout) ? solve() : null))
web.stderr.on("data", data => console.debug(`web > ${data}`)) web.stderr.on("data", data => console.debug(`web > ${data}`))
}) })
@@ -351,9 +351,9 @@ function quit(reason) {
//Base content //Base content
info.break() info.break()
const {base:parts, repositories:_repositories, ...base} = metadata.plugins.base.inputs.action({core, preset}) const {base: parts, repositories: _repositories, ...base} = metadata.plugins.base.inputs.action({core, preset})
conf.settings.repositories = _repositories conf.settings.repositories = _repositories
info.group({metadata, name:"base", inputs:{repositories:conf.settings.repositories, ...base}}) info.group({metadata, name: "base", inputs: {repositories: conf.settings.repositories, ...base}})
info("Base sections", parts) info("Base sections", parts)
base.base = false base.base = false
for (const part of conf.settings.plugins.base.parts) for (const part of conf.settings.plugins.base.parts)
@@ -364,7 +364,7 @@ function quit(reason) {
const plugins = {} const plugins = {}
for (const name of Object.keys(Plugins).filter(key => !["base", "core"].includes(key))) { for (const name of Object.keys(Plugins).filter(key => !["base", "core"].includes(key))) {
//Parse inputs //Parse inputs
const {[name]:enabled, ...inputs} = metadata.plugins[name].inputs.action({core, preset}) const {[name]: enabled, ...inputs} = metadata.plugins[name].inputs.action({core, preset})
plugins[name] = {enabled} plugins[name] = {enabled}
//Register user inputs //Register user inputs
if (enabled) { if (enabled) {
@@ -386,9 +386,9 @@ function quit(reason) {
info.break() info.break()
info.section("Rendering") info.section("Rendering")
let rendered = await retry(async () => { let rendered = await retry(async () => {
const {rendered} = await metrics({login:user, q}, {graphql, rest, plugins, conf, die, verify, convert}, {Plugins, Templates}) const {rendered} = await metrics({login: user, q}, {graphql, rest, plugins, conf, die, verify, convert}, {Plugins, Templates})
return rendered return rendered
}, {retries, delay:retries_delay}) }, {retries, delay: retries_delay})
if (!rendered) if (!rendered)
throw new Error("Could not render metrics") throw new Error("Could not render metrics")
info("Status", "complete") info("Status", "complete")
@@ -409,13 +409,13 @@ function quit(reason) {
let data = "" let data = ""
await retry(async () => { await retry(async () => {
try { try {
data = `${Buffer.from((await committer.rest.repos.getContent({...github.context.repo, ref:`heads/${committer.head}`, path:filename})).data.content, "base64")}` data = `${Buffer.from((await committer.rest.repos.getContent({...github.context.repo, ref: `heads/${committer.head}`, path: filename})).data.content, "base64")}`
} }
catch (error) { catch (error) {
if (error.response.status !== 404) if (error.response.status !== 404)
throw error throw error
} }
}, {retries:retries_output_action, delay:retries_delay_output_action}) }, {retries: retries_output_action, delay: retries_delay_output_action})
const previous = await svg.hash(data) const previous = await svg.hash(data)
info("Previous hash", previous) info("Previous hash", previous)
const current = await svg.hash(rendered) const current = await svg.hash(rendered)
@@ -430,7 +430,7 @@ function quit(reason) {
if (dryrun) if (dryrun)
info("Actions to perform", "(none)") info("Actions to perform", "(none)")
else { else {
await fs.mkdir(paths.dirname(paths.join("/renders", filename)), {recursive:true}) await fs.mkdir(paths.dirname(paths.join("/renders", filename)), {recursive: true})
await fs.writeFile(paths.join("/renders", filename), Buffer.from(typeof rendered === "object" ? JSON.stringify(rendered) : `${rendered}`)) await fs.writeFile(paths.join("/renders", filename), Buffer.from(typeof rendered === "object" ? JSON.stringify(rendered) : `${rendered}`))
info(`Save to /metrics_renders/${filename}`, "ok") info(`Save to /metrics_renders/${filename}`, "ok")
info("Output action", _action) info("Output action", _action)
@@ -454,7 +454,7 @@ function quit(reason) {
console.debug(`Processing ${path}`) console.debug(`Processing ${path}`)
let sha = null let sha = null
try { try {
const {repository:{object:{oid}}} = await graphql( const {repository: {object: {oid}}} = await graphql(
` `
query Sha { query Sha {
repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") { repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
@@ -462,7 +462,7 @@ function quit(reason) {
} }
} }
`, `,
{headers:{authorization:`token ${committer.token}`}}, {headers: {authorization: `token ${committer.token}`}},
) )
sha = oid sha = oid
} }
@@ -474,14 +474,14 @@ function quit(reason) {
...github.context.repo, ...github.context.repo,
path, path,
content, content,
message:`${committer.message} (cache)`, message: `${committer.message} (cache)`,
...(sha ? {sha} : {}), ...(sha ? {sha} : {}),
branch:committer.pr ? committer.head : committer.branch, branch: committer.pr ? committer.head : committer.branch,
}) })
rendered = rendered.replace(match, `<img src="https://github.com/${github.context.repo.owner}/${github.context.repo.repo}/blob/${committer.branch}/${path}">`) rendered = rendered.replace(match, `<img src="https://github.com/${github.context.repo.owner}/${github.context.repo.repo}/blob/${committer.branch}/${path}">`)
info(`Saving ${path}`, "ok") info(`Saving ${path}`, "ok")
} }
}, {retries:retries_output_action, delay:retries_delay_output_action}) }, {retries: retries_output_action, delay: retries_delay_output_action})
} }
} }
@@ -499,10 +499,10 @@ function quit(reason) {
//Upload to gist (this is done as user since committer_token may not have gist rights) //Upload to gist (this is done as user since committer_token may not have gist rights)
if (committer.gist) { if (committer.gist) {
await retry(async () => { await retry(async () => {
await rest.gists.update({gist_id:committer.gist, files:{[filename]:{content:rendered}}}) await rest.gists.update({gist_id: committer.gist, files: {[filename]: {content: rendered}}})
info(`Upload to gist ${committer.gist}`, "ok") info(`Upload to gist ${committer.gist}`, "ok")
committer.commit = false committer.commit = false
}, {retries:retries_output_action, delay:retries_delay_output_action}) }, {retries: retries_output_action, delay: retries_delay_output_action})
} }
//Commit metrics //Commit metrics
@@ -510,14 +510,14 @@ function quit(reason) {
await retry(async () => { await retry(async () => {
await committer.rest.repos.createOrUpdateFileContents({ await committer.rest.repos.createOrUpdateFileContents({
...github.context.repo, ...github.context.repo,
path:filename, path: filename,
message:committer.message, message: committer.message,
content:Buffer.from(typeof rendered === "object" ? JSON.stringify(rendered) : `${rendered}`).toString("base64"), content: Buffer.from(typeof rendered === "object" ? JSON.stringify(rendered) : `${rendered}`).toString("base64"),
branch:committer.pr ? committer.head : committer.branch, branch: committer.pr ? committer.head : committer.branch,
...(committer.sha ? {sha:committer.sha} : {}), ...(committer.sha ? {sha: committer.sha} : {}),
}) })
info(`Commit to branch ${committer.branch}`, "ok") info(`Commit to branch ${committer.branch}`, "ok")
}, {retries:retries_output_action, delay:retries_delay_output_action}) }, {retries: retries_output_action, delay: retries_delay_output_action})
} }
//Pull request //Pull request
@@ -526,7 +526,7 @@ function quit(reason) {
let number = null let number = null
await retry(async () => { await retry(async () => {
try { try {
({data:{number}} = await committer.rest.pulls.create({...github.context.repo, head:committer.head, base:committer.branch, title:`Auto-generated metrics for run #${github.context.runId}`, body:" ", maintainer_can_modify:true})) ;({data: {number}} = await committer.rest.pulls.create({...github.context.repo, head: committer.head, base: committer.branch, title: `Auto-generated metrics for run #${github.context.runId}`, body: " ", maintainer_can_modify: true}))
info(`Pull request from ${committer.head} to ${committer.branch}`, "(created)") info(`Pull request from ${committer.head} to ${committer.branch}`, "(created)")
} }
catch (error) { catch (error) {
@@ -535,7 +535,7 @@ function quit(reason) {
if (/A pull request already exists/.test(error)) { if (/A pull request already exists/.test(error)) {
info(`Pull request from ${committer.head} to ${committer.branch}`, "(already existing)") info(`Pull request from ${committer.head} to ${committer.branch}`, "(already existing)")
const q = `repo:${github.context.repo.owner}/${github.context.repo.repo}+type:pr+state:open+Auto-generated metrics for run #${github.context.runId}+in:title` const q = `repo:${github.context.repo.owner}/${github.context.repo.repo}+type:pr+state:open+Auto-generated metrics for run #${github.context.runId}+in:title`
const prs = (await committer.rest.search.issuesAndPullRequests({q})).data.items.filter(({user:{login}}) => login === "github-actions[bot]") const prs = (await committer.rest.search.issuesAndPullRequests({q})).data.items.filter(({user: {login}}) => login === "github-actions[bot]")
if (prs.length < 1) if (prs.length < 1)
throw new Error("0 matching prs. Cannot proceed.") throw new Error("0 matching prs. Cannot proceed.")
if (prs.length > 1) if (prs.length > 1)
@@ -548,12 +548,12 @@ function quit(reason) {
committer.merge = false committer.merge = false
number = "(none)" number = "(none)"
} }
else else {
throw error throw error
}
} }
info("Pull request number", number) info("Pull request number", number)
}, {retries:retries_output_action, delay:retries_delay_output_action}) }, {retries: retries_output_action, delay: retries_delay_output_action})
//Merge pull request //Merge pull request
if (committer.merge) { if (committer.merge) {
info("Merge method", committer.merge) info("Merge method", committer.merge)
@@ -561,7 +561,7 @@ function quit(reason) {
do { do {
const success = await retry(async () => { const success = await retry(async () => {
//Check pull request mergeability (https://octokit.github.io/rest.js/v18#pulls-get) //Check pull request mergeability (https://octokit.github.io/rest.js/v18#pulls-get)
const {data:{mergeable, mergeable_state:state}} = await committer.rest.pulls.get({...github.context.repo, pull_number:number}) const {data: {mergeable, mergeable_state: state}} = await committer.rest.pulls.get({...github.context.repo, pull_number: number})
console.debug(`Pull request #${number} mergeable state is "${state}"`) console.debug(`Pull request #${number} mergeable state is "${state}"`)
if (mergeable === null) { if (mergeable === null) {
await wait(15) await wait(15)
@@ -570,17 +570,17 @@ function quit(reason) {
if (!mergeable) if (!mergeable)
throw new Error(`Pull request #${number} is not mergeable (state is "${state}")`) throw new Error(`Pull request #${number} is not mergeable (state is "${state}")`)
//Merge pull request //Merge pull request
await committer.rest.pulls.merge({...github.context.repo, pull_number:number, merge_method:committer.merge}) await committer.rest.pulls.merge({...github.context.repo, pull_number: number, merge_method: committer.merge})
info(`Merge #${number} to ${committer.branch}`, "ok") info(`Merge #${number} to ${committer.branch}`, "ok")
return true return true
}, {retries:retries_output_action, delay:retries_delay_output_action}) }, {retries: retries_output_action, delay: retries_delay_output_action})
if (!success) if (!success)
continue continue
//Delete head branch //Delete head branch
await retry(async () => { await retry(async () => {
try { try {
await wait(15) await wait(15)
await committer.rest.git.deleteRef({...github.context.repo, ref:`heads/${committer.head}`}) await committer.rest.git.deleteRef({...github.context.repo, ref: `heads/${committer.head}`})
} }
catch (error) { catch (error) {
console.debug(error) console.debug(error)
@@ -588,7 +588,7 @@ function quit(reason) {
throw error throw error
} }
info(`Branch ${committer.head}`, "(deleted)") info(`Branch ${committer.head}`, "(deleted)")
}, {retries:retries_output_action, delay:retries_delay_output_action}) }, {retries: retries_output_action, delay: retries_delay_output_action})
break break
} while (--attempts) } while (--attempts)
} }
@@ -596,14 +596,14 @@ function quit(reason) {
} }
//Consumed API requests //Consumed API requests
if ((!mocked)&&(!/^NOT_NEEDED$/.test(token))) { if ((!mocked) && (!/^NOT_NEEDED$/.test(token))) {
info.break() info.break()
info.section("Consumed API requests") info.section("Consumed API requests")
info(" * provided that no other app used your quota during execution", "") info(" * provided that no other app used your quota during execution", "")
const {data:current} = await rest.rateLimit.get().catch(() => ({data:{resources:{}}})) const {data: current} = await rest.rateLimit.get().catch(() => ({data: {resources: {}}}))
for (const type of ["core", "graphql", "search"]) { for (const type of ["core", "graphql", "search"]) {
const used = resources[type].remaining - current.resources[type].remaining const used = resources[type].remaining - current.resources[type].remaining
info({core:"REST API", graphql:"GraphQL API", search:"Search API"}[type], (Number.isFinite(used)&&(used >= 0)) ? used : "(unknown)") info({core: "REST API", graphql: "GraphQL API", search: "Search API"}[type], (Number.isFinite(used) && (used >= 0)) ? used : "(unknown)")
} }
} }

View File

@@ -10,7 +10,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
//Debug //Debug
login = login.replace(/[\n\r]/g, "") login = login.replace(/[\n\r]/g, "")
console.debug(`metrics/compute/${login} > start`) console.debug(`metrics/compute/${login} > start`)
console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256})) console.debug(util.inspect(q, {depth: Infinity, maxStringLength: 256}))
//Load template //Load template
const template = q.template || conf.settings.templates.default const template = q.template || conf.settings.templates.default
@@ -24,14 +24,14 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
//Initialization //Initialization
const pending = [] const pending = []
const {queries} = conf const {queries} = conf
const extras = {css:(conf.settings.extras?.css ?? conf.settings.extras?.default) ? q["extras.css"] ?? "" : "", js:(conf.settings.extras?.js ?? conf.settings.extras?.default) ? q["extras.js"] ?? "" : ""} const extras = {css: (conf.settings.extras?.css ?? conf.settings.extras?.default) ? q["extras.css"] ?? "" : "", js: (conf.settings.extras?.js ?? conf.settings.extras?.default) ? q["extras.js"] ?? "" : ""}
const data = {q, animated:true, large:false, base:{}, config:{}, errors:[], plugins:{}, computed:{}, extras, postscripts:[]} const data = {q, animated: true, large: false, base: {}, config: {}, errors: [], plugins: {}, computed: {}, extras, postscripts: []}
const imports = { const imports = {
plugins:Plugins, plugins: Plugins,
templates:Templates, templates: Templates,
metadata:conf.metadata, metadata: conf.metadata,
...utils, ...utils,
...utils.formatters({timeZone:q["config.timezone"]}), ...utils.formatters({timeZone: q["config.timezone"]}),
...(/markdown/.test(convert) ...(/markdown/.test(convert)
? { ? {
imgb64(url, options) { imgb64(url, options) {
@@ -60,7 +60,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
//Executing base plugin and compute metrics //Executing base plugin and compute metrics
console.debug(`metrics/compute/${login} > compute`) console.debug(`metrics/compute/${login} > compute`)
await Plugins.base({login, q, data, rest, graphql, plugins, queries, pending, imports}, conf) await Plugins.base({login, q, data, rest, graphql, plugins, queries, pending, imports}, conf)
await computer({login, q}, {conf, data, rest, graphql, plugins, queries, account:data.account, convert, template}, {pending, imports}) await computer({login, q}, {conf, data, rest, graphql, plugins, queries, account: data.account, convert, template}, {pending, imports})
const promised = await Promise.all(pending) const promised = await Promise.all(pending)
//Check plugins errors //Check plugins errors
@@ -70,7 +70,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
if (die) if (die)
throw new Error("An error occured during rendering, dying") throw new Error("An error occured during rendering, dying")
else else
console.debug(util.inspect(errors, {depth:Infinity, maxStringLength:256})) console.debug(util.inspect(errors, {depth: Infinity, maxStringLength: 256}))
} }
//JSON output //JSON output
@@ -89,7 +89,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
} }
return value return value
})) }))
return {rendered, mime:"application/json"} return {rendered, mime: "application/json"}
} }
//Markdown output //Markdown output
@@ -100,12 +100,12 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
try { try {
let template = `${q.markdown}`.replace(/\n/g, "") let template = `${q.markdown}`.replace(/\n/g, "")
if (!/^https:/.test(template)) { if (!/^https:/.test(template)) {
const {data:{default_branch:branch, full_name:repo}} = await rest.repos.get({owner:login, repo:q.repo || login}) const {data: {default_branch: branch, full_name: repo}} = await rest.repos.get({owner: login, repo: q.repo || login})
console.debug(`metrics/compute/${login} > on ${repo} with default branch ${branch}`) console.debug(`metrics/compute/${login} > on ${repo} with default branch ${branch}`)
template = `https://raw.githubusercontent.com/${repo}/${branch}/${template}` template = `https://raw.githubusercontent.com/${repo}/${branch}/${template}`
} }
console.debug(`metrics/compute/${login} > fetching ${template}`) console.debug(`metrics/compute/${login} > fetching ${template}`)
;({data:source} = await imports.axios.get(template, {headers:{Accept:"text/plain"}})) ;({data: source} = await imports.axios.get(template, {headers: {Accept: "text/plain"}}))
} }
catch (error) { catch (error) {
console.debug(error) console.debug(error)
@@ -123,7 +123,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
console.debug(`metrics/compute/${login} > embed called with`) console.debug(`metrics/compute/${login} > embed called with`)
console.debug(q) console.debug(q)
let {base} = q let {base} = q
q = {..._q, ...Object.fromEntries(Object.keys(Plugins).map(key => [key, false])), ...Object.fromEntries(conf.settings.plugins.base.parts.map(part => [`base.${part}`, false])), template:q.repo ? "repository" : "classic", ...q} q = {..._q, ...Object.fromEntries(Object.keys(Plugins).map(key => [key, false])), ...Object.fromEntries(conf.settings.plugins.base.parts.map(part => [`base.${part}`, false])), template: q.repo ? "repository" : "classic", ...q}
//Translate action syntax to web syntax //Translate action syntax to web syntax
let parts = [] let parts = []
if (base === true) if (base === true)
@@ -140,33 +140,33 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
} }
q = Object.fromEntries([...Object.entries(q).map(([key, value]) => [key.replace(/^plugin_/, "").replace(/_/g, "."), value]), ["base", false]]) q = Object.fromEntries([...Object.entries(q).map(([key, value]) => [key.replace(/^plugin_/, "").replace(/_/g, "."), value]), ["base", false]])
//Compute rendering //Compute rendering
const {rendered} = await metrics({login, q}, {...arguments[1], convert:["svg", "png", "jpeg"].includes(q["config.output"]) ? q["config.output"] : null}, arguments[2]) const {rendered} = await metrics({login, q}, {...arguments[1], convert: ["svg", "png", "jpeg"].includes(q["config.output"]) ? q["config.output"] : null}, arguments[2])
console.debug(`metrics/compute/${login}/embed > ${name} > success >>>>>>>>>>>>>>>>>>>>>>`) console.debug(`metrics/compute/${login}/embed > ${name} > success >>>>>>>>>>>>>>>>>>>>>>`)
return `<img class="metrics-cachable" data-name="${name}" src="data:image/${{png:"png", jpeg:"jpeg"}[q["config.output"]] ?? "svg+xml"};base64,${Buffer.from(rendered).toString("base64")}">` return `<img class="metrics-cachable" data-name="${name}" src="data:image/${{png: "png", jpeg: "jpeg"}[q["config.output"]] ?? "svg+xml"};base64,${Buffer.from(rendered).toString("base64")}">`
} }
//Rendering template source //Rendering template source
let rendered = source.replace(/\{\{ (?<content>[\s\S]*?) \}\}/g, "{%= $<content> %}") let rendered = source.replace(/\{\{ (?<content>[\s\S]*?) \}\}/g, "{%= $<content> %}")
console.debug(rendered) console.debug(rendered)
for (const delimiters of [{openDelimiter:"<", closeDelimiter:">"}, {openDelimiter:"{", closeDelimiter:"}"}]) for (const delimiters of [{openDelimiter: "<", closeDelimiter: ">"}, {openDelimiter: "{", closeDelimiter: "}"}])
rendered = await ejs.render(rendered, {...data, s:imports.s, f:imports.format, embed}, {views, async:true, ...delimiters}) rendered = await ejs.render(rendered, {...data, s: imports.s, f: imports.format, embed}, {views, async: true, ...delimiters})
console.debug(`metrics/compute/${login} > success`) console.debug(`metrics/compute/${login} > success`)
//Output //Output
if (convert === "markdown-pdf") { if (convert === "markdown-pdf") {
return imports.svg.pdf(rendered, { return imports.svg.pdf(rendered, {
paddings:q["config.padding"] || conf.settings.padding, paddings: q["config.padding"] || conf.settings.padding,
style:extras.css, style: extras.css,
twemojis:q["config.twemoji"], twemojis: q["config.twemoji"],
gemojis:q["config.gemoji"], gemojis: q["config.gemoji"],
octicons:q["config.octicon"], octicons: q["config.octicon"],
rest, rest,
}) })
} }
return {rendered, mime:"text/html"} return {rendered, mime: "text/html"}
} }
//Rendering //Rendering
console.debug(`metrics/compute/${login} > render`) console.debug(`metrics/compute/${login} > render`)
let rendered = await ejs.render(image, {...data, s:imports.s, f:imports.format, style, fonts}, {views, async:true}) let rendered = await ejs.render(image, {...data, s: imports.s, f: imports.format, style, fonts}, {views, async: true})
//Additional transformations //Additional transformations
if (q["config.twemoji"]) if (q["config.twemoji"])
@@ -192,7 +192,7 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
console.debug(`metrics/compute/${login} > verified SVG, no parsing errors found`) console.debug(`metrics/compute/${login} > verified SVG, no parsing errors found`)
} }
//Resizing //Resizing
const {resized, mime} = await imports.svg.resize(rendered, {paddings:q["config.padding"] || conf.settings.padding, convert:convert === "svg" ? null : convert, scripts:[...data.postscripts, extras.js || null].filter(x => x)}) const {resized, mime} = await imports.svg.resize(rendered, {paddings: q["config.padding"] || conf.settings.padding, convert: convert === "svg" ? null : convert, scripts: [...data.postscripts, extras.js || null].filter(x => x)})
rendered = resized rendered = resized
//Result //Result
@@ -212,37 +212,37 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
//Metrics insights //Metrics insights
metrics.insights = async function({login}, {graphql, rest, conf}, {Plugins, Templates}) { metrics.insights = async function({login}, {graphql, rest, conf}, {Plugins, Templates}) {
const q = { const q = {
template:"classic", template: "classic",
achievements:true, achievements: true,
"achievements.threshold":"X", "achievements.threshold": "X",
isocalendar:true, isocalendar: true,
"isocalendar.duration":"full-year", "isocalendar.duration": "full-year",
languages:true, languages: true,
"languages.limit":0, "languages.limit": 0,
activity:true, activity: true,
"activity.limit":100, "activity.limit": 100,
"activity.days":0, "activity.days": 0,
notable:true, notable: true,
followup:true, followup: true,
"followup.sections":"repositories, user", "followup.sections": "repositories, user",
habits:true, habits: true,
"habits.from":100, "habits.from": 100,
"habits.days":7, "habits.days": 7,
"habits.facts":false, "habits.facts": false,
"habits.charts":true, "habits.charts": true,
introduction:true, introduction: true,
} }
const plugins = { const plugins = {
achievements:{enabled:true}, achievements: {enabled: true},
isocalendar:{enabled:true}, isocalendar: {enabled: true},
languages:{enabled:true, extras:false}, languages: {enabled: true, extras: false},
activity:{enabled:true, markdown:"extended"}, activity: {enabled: true, markdown: "extended"},
notable:{enabled:true}, notable: {enabled: true},
followup:{enabled:true}, followup: {enabled: true},
habits:{enabled:true, extras:false}, habits: {enabled: true, extras: false},
introduction:{enabled:true}, introduction: {enabled: true},
} }
return metrics({login, q}, {graphql, rest, plugins, conf, convert:"json"}, {Plugins, Templates}) return metrics({login, q}, {graphql, rest, plugins, conf, convert: "json"}, {Plugins, Templates})
} }
//Metrics insights static render //Metrics insights static render
@@ -260,7 +260,7 @@ metrics.insights.output = async function({login, imports, conf}, {graphql, rest,
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`) await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
await page.evaluate(async json => localStorage.setItem("local.metrics", json), json) //eslint-disable-line no-undef await page.evaluate(async json => localStorage.setItem("local.metrics", json), json) //eslint-disable-line no-undef
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`) await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
await page.waitForSelector(".container .user", {timeout:10 * 60 * 1000}) await page.waitForSelector(".container .user", {timeout: 10 * 60 * 1000})
//Rendering //Rendering
console.debug(`metrics/compute/${login} > insights > rendering data`) console.debug(`metrics/compute/${login} > insights > rendering data`)
@@ -273,9 +273,9 @@ metrics.insights.output = async function({login, imports, conf}, {graphql, rest,
</head> </head>
<body> <body>
${await page.evaluate(() => document.querySelector("main").outerHTML)} ${await page.evaluate(() => document.querySelector("main").outerHTML)}
${(await Promise.all([".css/style.vars.css", ".css/style.css", "about/.statics/style.css"].map(path => utils.axios.get(`${server}/${path}`)))).map(({data:style}) => `<style>${style}</style>`).join("\n")} ${(await Promise.all([".css/style.vars.css", ".css/style.css", "about/.statics/style.css"].map(path => utils.axios.get(`${server}/${path}`)))).map(({data: style}) => `<style>${style}</style>`).join("\n")}
</body> </body>
</html>` </html>`
await browser.close() await browser.close()
return {mime:"text/html", rendered} return {mime: "text/html", rendered}
} }

View File

@@ -1,7 +1,7 @@
//Imports //Imports
import fs from "fs" import fs from "fs"
import yaml from "js-yaml" import yaml from "js-yaml"
import {marked} from "marked" import { marked } from "marked"
import fetch from "node-fetch" import fetch from "node-fetch"
import path from "path" import path from "path"
import url from "url" import url from "url"
@@ -13,7 +13,7 @@ const categories = ["core", "github", "social", "community"]
let previous = null let previous = null
//Environment //Environment
const env = {ghactions:`${process.env.GITHUB_ACTIONS}` === "true"} const env = {ghactions: `${process.env.GITHUB_ACTIONS}` === "true"}
/**Metadata descriptor parser */ /**Metadata descriptor parser */
export default async function metadata({log = true, diff = false} = {}) { export default async function metadata({log = true, diff = false} = {}) {
@@ -50,7 +50,7 @@ export default async function metadata({log = true, diff = false} = {}) {
if (!(await fs.promises.lstat(path.join(___plugins, name))).isDirectory()) if (!(await fs.promises.lstat(path.join(___plugins, name))).isDirectory())
continue continue
logger(`metrics/metadata > loading plugin metadata [community/${name}]`) logger(`metrics/metadata > loading plugin metadata [community/${name}]`)
Plugins[name] = await metadata.plugin({__plugins:___plugins, __templates, name, logger}) Plugins[name] = await metadata.plugin({__plugins: ___plugins, __templates, name, logger})
Plugins[name].community = true Plugins[name].community = true
} }
continue continue
@@ -84,7 +84,7 @@ export default async function metadata({log = true, diff = false} = {}) {
const descriptor = yaml.load(`${await fs.promises.readFile(__descriptor, "utf-8")}`) const descriptor = yaml.load(`${await fs.promises.readFile(__descriptor, "utf-8")}`)
//Metadata //Metadata
return {plugins:Plugins, templates:Templates, packaged, descriptor, env} return {plugins: Plugins, templates: Templates, packaged, descriptor, env}
} }
/**Metadata extractor for inputs */ /**Metadata extractor for inputs */
@@ -104,14 +104,14 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
//Inputs parser //Inputs parser
{ {
meta.inputs = function({data:{user = null} = {}, q, account}, defaults = {}) { meta.inputs = function({data: {user = null} = {}, q, account}, defaults = {}) {
//Support check //Support check
if (!account) if (!account)
console.debug(`metrics/inputs > account type not set for plugin ${name}!`) console.debug(`metrics/inputs > account type not set for plugin ${name}!`)
if (account !== "bypass") { if (account !== "bypass") {
const context = q.repo ? "repository" : account const context = q.repo ? "repository" : account
if (!meta.supports?.includes(context)) if (!meta.supports?.includes(context))
throw {error:{message:`Not supported for: ${context}`, instance:new Error()}} throw {error: {message: `Not supported for: ${context}`, instance: new Error()}}
} }
//Special values replacer //Special values replacer
const replacer = value => { const replacer = value => {
@@ -128,7 +128,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
} }
//Inputs checks //Inputs checks
const result = Object.fromEntries( const result = Object.fromEntries(
Object.entries(inputs).map(([key, {type, format, default:defaulted, min, max, values, inherits:_inherits}]) => [ Object.entries(inputs).map(([key, {type, format, default: defaulted, min, max, values, inherits: _inherits}]) => [
//Format key //Format key
metadata.to.query(key, {name}), metadata.to.query(key, {name}),
//Format value //Format value
@@ -165,7 +165,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
logger(`metrics/inputs > failed to decode uri : ${value}`) logger(`metrics/inputs > failed to decode uri : ${value}`)
value = defaulted value = defaulted
} }
const separators = {"comma-separated":",", "space-separated":" "} const separators = {"comma-separated": ",", "space-separated": " "}
const separator = separators[[format].flat().filter(s => s in separators)[0]] ?? "," const separator = separators[[format].flat().filter(s => s in separators)[0]] ?? ","
return value.split(separator).map(v => replacer(v).toLocaleLowerCase()).filter(v => Array.isArray(values) ? values.includes(v) : true).filter(v => v) return value.split(separator).map(v => replacer(v).toLocaleLowerCase()).filter(v => Array.isArray(values) ? values.includes(v) : true).filter(v => v)
} }
@@ -230,13 +230,14 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
Object.entries(inputs).map(([key, value]) => [ Object.entries(inputs).map(([key, value]) => [
key, key,
{ {
comment:"", comment: "",
descriptor:yaml.dump({ descriptor: yaml.dump({
[key]:Object.fromEntries( [key]: Object.fromEntries(
Object.entries(value).filter(([key]) => ["description", "default", "required"].includes(key)).map(([k, v]) => k === "description" ? [k, v.split("\n")[0]] : k === "default" ? [k, ((/^\$\{\{[\s\S]+\}\}$/.test(v)) || (["config_presets", "config_timezone", "use_prebuilt_image"].includes(key))) ? v : "<default-value>"] : [k, v] Object.entries(value).filter(([key]) => ["description", "default", "required"].includes(key)).map(([k, v]) =>
k === "description" ? [k, v.split("\n")[0]] : k === "default" ? [k, ((/^\$\{\{[\s\S]+\}\}$/.test(v)) || (["config_presets", "config_timezone", "use_prebuilt_image"].includes(key))) ? v : "<default-value>"] : [k, v]
), ),
), ),
}, {quotingType:'"', noCompatMode:true}), }, {quotingType: '"', noCompatMode: true}),
}, },
]), ]),
) )
@@ -258,9 +259,9 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
value = "<default-value>" value = "<default-value>"
} }
} }
else else {
value = process.env[`INPUT_${key.toUpperCase()}`]?.trim() ?? "<default-value>" value = process.env[`INPUT_${key.toUpperCase()}`]?.trim() ?? "<default-value>"
}
const unspecified = value === "<default-value>" const unspecified = value === "<default-value>"
//From presets //From presets
@@ -279,32 +280,32 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
q[key] = value q[key] = value
} }
} }
return meta.inputs({q, account:"bypass"}) return meta.inputs({q, account: "bypass"})
} }
} }
//Web metadata //Web metadata
{ {
meta.web = Object.fromEntries( meta.web = Object.fromEntries(
Object.entries(inputs).map(([key, {type, description:text, example, default:defaulted, min = 0, max = 9999, values}]) => [ Object.entries(inputs).map(([key, {type, description: text, example, default: defaulted, min = 0, max = 9999, values}]) => [
//Format key //Format key
metadata.to.query(key), metadata.to.query(key),
//Value descriptor //Value descriptor
(() => { (() => {
switch (type) { switch (type) {
case "boolean": case "boolean":
return {text, type:"boolean", defaulted:/^(?:[Tt]rue|[Oo]n|[Yy]es|1)$/.test(defaulted) ? true : /^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(defaulted) ? false : defaulted} return {text, type: "boolean", defaulted: /^(?:[Tt]rue|[Oo]n|[Yy]es|1)$/.test(defaulted) ? true : /^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(defaulted) ? false : defaulted}
case "number": case "number":
return {text, type:"number", min, max, defaulted} return {text, type: "number", min, max, defaulted}
case "array": case "array":
return {text, type:"text", placeholder:example ?? defaulted, defaulted} return {text, type: "text", placeholder: example ?? defaulted, defaulted}
case "string": { case "string": {
if (Array.isArray(values)) if (Array.isArray(values))
return {text, type:"select", values, defaulted} return {text, type: "select", values, defaulted}
return {text, type:"text", placeholder:example ?? defaulted, defaulted} return {text, type: "text", placeholder: example ?? defaulted, defaulted}
} }
case "json": case "json":
return {text, type:"text", placeholder:example ?? defaulted, defaulted} return {text, type: "text", placeholder: example ?? defaulted, defaulted}
default: default:
return null return null
} }
@@ -317,7 +318,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
{ {
//Extract demos //Extract demos
const raw = `${await fs.promises.readFile(path.join(__plugins, name, "README.md"), "utf-8")}` const raw = `${await fs.promises.readFile(path.join(__plugins, name, "README.md"), "utf-8")}`
const demo = meta.examples ? demos({examples:meta.examples}) : raw.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? "<td></td>" const demo = meta.examples ? demos({examples: meta.examples}) : raw.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? "<td></td>"
//Compatibility //Compatibility
const templates = {} const templates = {}
@@ -337,7 +338,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
const header = [ const header = [
"<table>", "<table>",
` <tr><th colspan="2"><h3>${meta.name}</h3></th></tr>`, ` <tr><th colspan="2"><h3>${meta.name}</h3></th></tr>`,
` <tr><td colspan="2" align="center">${marked.parse(meta.description ?? "", {silent:true})}</td></tr>`, ` <tr><td colspan="2" align="center">${marked.parse(meta.description ?? "", {silent: true})}</td></tr>`,
meta.authors?.length ? `<tr><th>Authors</th><td>${[meta.authors].flat().map(author => `<a href="https://github.com/${author}">@${author}</a>`)}</td></tr>` : "", meta.authors?.length ? `<tr><th>Authors</th><td>${[meta.authors].flat().map(author => `<a href="https://github.com/${author}">@${author}</a>`)}</td></tr>` : "",
" <tr>", " <tr>",
' <th rowspan="3">Supported features<br><sub><a href="metadata.yml">→ Full specification</a></sub></th>', ' <th rowspan="3">Supported features<br><sub><a href="metadata.yml">→ Full specification</a></sub></th>',
@@ -353,14 +354,16 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
}</td>`, }</td>`,
" </tr>", " </tr>",
" <tr>", " <tr>",
` <td>${[ ` <td>${
...(meta.scopes ?? []).map(scope => `<code>🔑 ${{public_access:"(scopeless)"}[scope] ?? scope}</code>`), [
...Object.entries(inputs).filter(([_, {type}]) => type === "token").map(([token]) => `<code>🗝 ${token}</code>`), ...(meta.scopes ?? []).map(scope => `<code>🔑 ${{public_access: "(scopeless)"}[scope] ?? scope}</code>`),
...(meta.scopes?.length ? ["read:org", "read:user", "read:packages", "repo"].map(scope => !meta.scopes.includes(scope) ? `<code>${scope} (optional)</code>` : null).filter(v => v) : []), ...Object.entries(inputs).filter(([_, {type}]) => type === "token").map(([token]) => `<code>🗝 ${token}</code>`),
].filter(v => v).join(" ") || "<i>No tokens are required for this plugin</i>"}</td>`, ...(meta.scopes?.length ? ["read:org", "read:user", "read:packages", "repo"].map(scope => !meta.scopes.includes(scope) ? `<code>${scope} (optional)</code>` : null).filter(v => v) : []),
].filter(v => v).join(" ") || "<i>No tokens are required for this plugin</i>"
}</td>`,
" </tr>", " </tr>",
" <tr>", " <tr>",
demos({colspan:2, wrap:name === "base", examples:meta.examples}), demos({colspan: 2, wrap: name === "base", examples: meta.examples}),
" </tr>", " </tr>",
"</table>", "</table>",
].filter(v => v).join("\n") ].filter(v => v).join("\n")
@@ -415,7 +418,7 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
cell.push(`<b>allowed values:</b><ul>${o.values.map(value => `<li>${value}</li>`).join("")}</ul>`) cell.push(`<b>allowed values:</b><ul>${o.values.map(value => `<li>${value}</li>`).join("")}</ul>`)
return ` <tr> return ` <tr>
<td nowrap="nowrap"><h4><code>${option}</code></h4></td> <td nowrap="nowrap"><h4><code>${option}</code></h4></td>
<td rowspan="2">${marked.parse(description, {silent:true})}<img width="900" height="1" alt=""></td> <td rowspan="2">${marked.parse(description, {silent: true})}<img width="900" height="1" alt=""></td>
</tr> </tr>
<tr> <tr>
<td nowrap="nowrap">${cell.join("\n")}</td> <td nowrap="nowrap">${cell.join("\n")}</td>
@@ -462,7 +465,7 @@ metadata.template = async function({__templates, name, plugins}) {
const header = [ const header = [
"<table>", "<table>",
` <tr><th colspan="2"><h3>${meta.name ?? "(unnamed template)"}</h3></th></tr>`, ` <tr><th colspan="2"><h3>${meta.name ?? "(unnamed template)"}</h3></th></tr>`,
` <tr><td colspan="2" align="center">${marked.parse(meta.description ?? "", {silent:true})}</td></tr>`, ` <tr><td colspan="2" align="center">${marked.parse(meta.description ?? "", {silent: true})}</td></tr>`,
" <tr>", " <tr>",
' <th rowspan="3">Supported features<br><sub><a href="metadata.yml">→ Full specification</a></sub></th>', ' <th rowspan="3">Supported features<br><sub><a href="metadata.yml">→ Full specification</a></sub></th>',
` <td>${Object.entries(compatibility).filter(([_, value]) => value).map(([id]) => `<a href="/source/plugins/${id}/README.md" title="${plugins[id].name}">${plugins[id].icon}</a>`).join(" ")}${meta.formats?.includes("markdown") ? " <code>✓ embed()</code>" : ""}</td>`, ` <td>${Object.entries(compatibility).filter(([_, value]) => value).map(([id]) => `<a href="/source/plugins/${id}/README.md" title="${plugins[id].name}">${plugins[id].icon}</a>`).join(" ")}${meta.formats?.includes("markdown") ? " <code>✓ embed()</code>" : ""}</td>`,
@@ -489,24 +492,24 @@ metadata.template = async function({__templates, name, plugins}) {
}</td>`, }</td>`,
" </tr>", " </tr>",
" <tr>", " <tr>",
demos({colspan:2, examples:meta.examples}), demos({colspan: 2, examples: meta.examples}),
" </tr>", " </tr>",
"</table>", "</table>",
].join("\n") ].join("\n")
//Result //Result
return { return {
name:meta.name ?? "(unnamed template)", name: meta.name ?? "(unnamed template)",
description:meta.description ?? "", description: meta.description ?? "",
index:meta.index ?? null, index: meta.index ?? null,
formats:meta.formats ?? null, formats: meta.formats ?? null,
supports:meta.supports ?? null, supports: meta.supports ?? null,
readme:{ readme: {
demo:demos({examples:meta.examples}), demo: demos({examples: meta.examples}),
compatibility:{ compatibility: {
...Object.fromEntries(Object.entries(compatibility).filter(([_, value]) => value)), ...Object.fromEntries(Object.entries(compatibility).filter(([_, value]) => value)),
...Object.fromEntries(Object.entries(compatibility).filter(([_, value]) => !value).map(([key, value]) => [key, meta.formats?.includes("markdown") ? "embed" : value])), ...Object.fromEntries(Object.entries(compatibility).filter(([_, value]) => !value).map(([key, value]) => [key, meta.formats?.includes("markdown") ? "embed" : value])),
base:true, base: true,
}, },
header, header,
}, },

View File

@@ -7,8 +7,8 @@ import metadata from "./metadata.mjs"
/**Presets parser */ /**Presets parser */
export default async function presets(list, {log = true, core = null} = {}) { export default async function presets(list, {log = true, core = null} = {}) {
//Init //Init
const {plugins} = await metadata({log:false}) const {plugins} = await metadata({log: false})
const {"config.presets":files} = plugins.core.inputs({q:{"config.presets":list}, account:"bypass"}) const {"config.presets": files} = plugins.core.inputs({q: {"config.presets": list}, account: "bypass"})
const logger = log ? console.debug : () => null const logger = log ? console.debug : () => null
const allowed = Object.entries(metadata.inputs).filter(([_, {type, preset}]) => (type !== "token") && (!/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(preset))).map(([key]) => key) const allowed = Object.entries(metadata.inputs).filter(([_, {type, preset}]) => (type !== "token") && (!/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(preset))).map(([key]) => key)
const env = core ? "action" : "web" const env = core ? "action" : "web"
@@ -36,7 +36,7 @@ export default async function presets(list, {log = true, core = null} = {}) {
logger(`metrics/presets > ${file} cannot be loaded in current environment ${env}, skipping`) logger(`metrics/presets > ${file} cannot be loaded in current environment ${env}, skipping`)
continue continue
} }
const {schema, with:inputs} = yaml.load(text) const {schema, with: inputs} = yaml.load(text)
logger(`metrics/presets > ${file} preset schema is ${schema}`) logger(`metrics/presets > ${file} preset schema is ${schema}`)
//Evaluate preset //Evaluate preset

View File

@@ -27,15 +27,15 @@ export default async function({log = true, sandbox = false, community = {}} = {}
const logger = log ? console.debug : () => null const logger = log ? console.debug : () => null
logger("metrics/setup > setup") logger("metrics/setup > setup")
const conf = { const conf = {
authenticated:null, authenticated: null,
templates:{}, templates: {},
queries:{}, queries: {},
settings:{port:3000}, settings: {port: 3000},
metadata:{}, metadata: {},
paths:{ paths: {
statics:__statics, statics: __statics,
templates:__templates, templates: __templates,
node_modules:__modules, node_modules: __modules,
}, },
} }
@@ -64,13 +64,13 @@ export default async function({log = true, sandbox = false, community = {}} = {}
} }
if (!conf.settings.templates) if (!conf.settings.templates)
conf.settings.templates = {default:"classic", enabled:[]} conf.settings.templates = {default: "classic", enabled: []}
if (!conf.settings.plugins) if (!conf.settings.plugins)
conf.settings.plugins = {} conf.settings.plugins = {}
conf.settings.community = {...conf.settings.community, ...community} conf.settings.community = {...conf.settings.community, ...community}
conf.settings.plugins.base = {parts:["header", "activity", "community", "repositories", "metadata"]} conf.settings.plugins.base = {parts: ["header", "activity", "community", "repositories", "metadata"]}
if (conf.settings.debug) if (conf.settings.debug)
logger(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256})) logger(util.inspect(conf.settings, {depth: Infinity, maxStringLength: 256}))
//Load package settings //Load package settings
logger("metrics/setup > load package.json") logger("metrics/setup > load package.json")
@@ -85,7 +85,7 @@ export default async function({log = true, sandbox = false, community = {}} = {}
if ((Array.isArray(conf.settings.community.templates)) && (conf.settings.community.templates.length)) { if ((Array.isArray(conf.settings.community.templates)) && (conf.settings.community.templates.length)) {
//Clean remote repository //Clean remote repository
logger(`metrics/setup > ${conf.settings.community.templates.length} community templates to install`) logger(`metrics/setup > ${conf.settings.community.templates.length} community templates to install`)
await fs.promises.rm(path.join(__templates, ".community"), {recursive:true, force:true}) await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
//Download community templates //Download community templates
for (const template of conf.settings.community.templates) { for (const template of conf.settings.community.templates) {
try { try {
@@ -95,10 +95,10 @@ export default async function({log = true, sandbox = false, community = {}} = {}
const command = `git clone --single-branch --branch ${branch} https://github.com/${repo}.git ${path.join(__templates, ".community")}` const command = `git clone --single-branch --branch ${branch} https://github.com/${repo}.git ${path.join(__templates, ".community")}`
logger(`metrics/setup > run ${command}`) logger(`metrics/setup > run ${command}`)
//Clone remote repository //Clone remote repository
processes.execSync(command, {stdio:"ignore"}) processes.execSync(command, {stdio: "ignore"})
//Extract template //Extract template
logger(`metrics/setup > extract ${name} from ${repo}@${branch}`) logger(`metrics/setup > extract ${name} from ${repo}@${branch}`)
await fs.promises.rm(path.join(__templates, `@${name}`), {recursive:true, force:true}) await fs.promises.rm(path.join(__templates, `@${name}`), {recursive: true, force: true})
await fs.promises.rename(path.join(__templates, ".community/source/templates", name), path.join(__templates, `@${name}`)) await fs.promises.rename(path.join(__templates, ".community/source/templates", name), path.join(__templates, `@${name}`))
//JavaScript file //JavaScript file
if (trust) if (trust)
@@ -113,18 +113,18 @@ export default async function({log = true, sandbox = false, community = {}} = {}
logger(`metrics/setup > @${name} extended from ${inherit}`) logger(`metrics/setup > @${name} extended from ${inherit}`)
await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs")) await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs"))
} }
else else {
logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`) logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`)
}
} }
} }
else else {
logger(`metrics/setup > @${name}/template.mjs does not exist`) logger(`metrics/setup > @${name}/template.mjs does not exist`)
}
//Clean remote repository //Clean remote repository
logger(`metrics/setup > clean ${repo}@${branch}`) logger(`metrics/setup > clean ${repo}@${branch}`)
await fs.promises.rm(path.join(__templates, ".community"), {recursive:true, force:true}) await fs.promises.rm(path.join(__templates, ".community"), {recursive: true, force: true})
logger(`metrics/setup > loaded community template ${name}`) logger(`metrics/setup > loaded community template ${name}`)
} }
catch (error) { catch (error) {
@@ -133,9 +133,9 @@ export default async function({log = true, sandbox = false, community = {}} = {}
} }
} }
} }
else else {
logger("metrics/setup > no community templates to install") logger("metrics/setup > no community templates to install")
}
//Load templates //Load templates
for (const name of await fs.promises.readdir(__templates)) { for (const name of await fs.promises.readdir(__templates)) {
@@ -145,10 +145,10 @@ export default async function({log = true, sandbox = false, community = {}} = {}
continue continue
logger(`metrics/setup > load template [${name}]`) logger(`metrics/setup > load template [${name}]`)
//Cache templates files //Cache templates files
const files = ["image.svg", "style.css", "fonts.css"].map(file => path.join(__templates, (fs.existsSync(path.join(directory, file)) ? name : "classic"), file)) const files = ["image.svg", "style.css", "fonts.css"].map(file => path.join(__templates, fs.existsSync(path.join(directory, file)) ? name : "classic", file))
const [image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(file)}`)) const [image, style, fonts] = await Promise.all(files.map(async file => `${await fs.promises.readFile(file)}`))
const partials = JSON.parse(`${await fs.promises.readFile(path.join(directory, "partials/_.json"))}`) const partials = JSON.parse(`${await fs.promises.readFile(path.join(directory, "partials/_.json"))}`)
conf.templates[name] = {image, style, fonts, partials, views:[directory]} conf.templates[name] = {image, style, fonts, partials, views: [directory]}
//Cache templates scripts //Cache templates scripts
Templates[name] = await (async () => { Templates[name] = await (async () => {
@@ -165,7 +165,7 @@ export default async function({log = true, sandbox = false, community = {}} = {}
const [image, style, fonts] = files.map(file => `${fs.readFileSync(file)}`) const [image, style, fonts] = files.map(file => `${fs.readFileSync(file)}`)
const partials = JSON.parse(`${fs.readFileSync(path.join(directory, "partials/_.json"))}`) const partials = JSON.parse(`${fs.readFileSync(path.join(directory, "partials/_.json"))}`)
logger(`metrics/setup > reload template [${name}] > success`) logger(`metrics/setup > reload template [${name}] > success`)
return {image, style, fonts, partials, views:[directory]} return {image, style, fonts, partials, views: [directory]}
}, },
}) })
} }
@@ -177,7 +177,7 @@ export default async function({log = true, sandbox = false, community = {}} = {}
case "community": { case "community": {
const ___plugins = path.join(__plugins, "community") const ___plugins = path.join(__plugins, "community")
for (const name of await fs.promises.readdir(___plugins)) for (const name of await fs.promises.readdir(___plugins))
await load.plugin(name, {__plugins:___plugins, Plugins, conf, logger}) await load.plugin(name, {__plugins: ___plugins, Plugins, conf, logger})
continue continue
} }
default: default:
@@ -191,7 +191,7 @@ export default async function({log = true, sandbox = false, community = {}} = {}
//Store authenticated user //Store authenticated user
if (conf.settings.token) { if (conf.settings.token) {
try { try {
conf.authenticated = (await (new OctokitRest.Octokit({auth:conf.settings.token})).users.getAuthenticated()).data.login conf.authenticated = (await (new OctokitRest.Octokit({auth: conf.settings.token})).users.getAuthenticated()).data.login
logger(`metrics/setup > setup > authenticated as ${conf.authenticated}`) logger(`metrics/setup > setup > authenticated as ${conf.authenticated}`)
} }
catch (error) { catch (error) {
@@ -251,7 +251,8 @@ const load = {
} }
} }
//Create queries formatters //Create queries formatters
Object.keys(queries).map(query => queries[query.substring(1)] = (vars = {}) => { Object.keys(queries).map(query =>
queries[query.substring(1)] = (vars = {}) => {
let queried = queries[query] let queried = queries[query]
for (const [key, value] of Object.entries(vars)) for (const [key, value] of Object.entries(vars))
queried = queried.replace(new RegExp(`[$]${key}`, "g"), value) queried = queried.replace(new RegExp(`[$]${key}`, "g"), value)

View File

@@ -1,17 +1,16 @@
//Imports //Imports
import octicons from "@primer/octicons" import octicons from "@primer/octicons"
import fs from "fs/promises"
import prism_lang from "prismjs/components/index.js"
import axios from "axios" import axios from "axios"
import processes from "child_process" import processes from "child_process"
import crypto from "crypto" import crypto from "crypto"
import {minify as csso} from "csso" import { minify as csso } from "csso"
import emoji from "emoji-name-map" import emoji from "emoji-name-map"
import fss from "fs" import fss from "fs"
import fs from "fs/promises"
import GIFEncoder from "gifencoder" import GIFEncoder from "gifencoder"
import jimp from "jimp" import jimp from "jimp"
import linguist from "linguist-js" import linguist from "linguist-js"
import {marked} from "marked" import { marked } from "marked"
import minimatch from "minimatch" import minimatch from "minimatch"
import nodechartist from "node-chartist" import nodechartist from "node-chartist"
import fetch from "node-fetch" import fetch from "node-fetch"
@@ -20,6 +19,7 @@ import os from "os"
import paths from "path" import paths from "path"
import PNG from "png-js" import PNG from "png-js"
import prism from "prismjs" import prism from "prismjs"
import prism_lang from "prismjs/components/index.js"
import _puppeteer from "puppeteer" import _puppeteer from "puppeteer"
import purgecss from "purgecss" import purgecss from "purgecss"
import readline from "readline" import readline from "readline"
@@ -34,7 +34,7 @@ import xmlformat from "xml-formatter"
prism_lang() prism_lang()
//Exports //Exports
export {axios, emoji, fetch, fs, git, jimp, minimatch, opengraph, os, paths, processes, rss, url, util} export { axios, emoji, fetch, fs, git, jimp, minimatch, opengraph, os, paths, processes, rss, url, util }
/**Returns module __dirname */ /**Returns module __dirname */
export function __module(module) { export function __module(module) {
@@ -45,25 +45,25 @@ export function __module(module) {
export const puppeteer = { export const puppeteer = {
async launch() { async launch() {
return _puppeteer.launch({ return _puppeteer.launch({
headless:this.headless, headless: this.headless,
executablePath:process.env.PUPPETEER_BROWSER_PATH, executablePath: process.env.PUPPETEER_BROWSER_PATH,
args:this.headless ? ["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] : [], args: this.headless ? ["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] : [],
ignoreDefaultArgs:["--disable-extensions"], ignoreDefaultArgs: ["--disable-extensions"],
}) })
}, },
headless:true, headless: true,
} }
/**Plural formatter */ /**Plural formatter */
export function s(value, end = "") { export function s(value, end = "") {
return value !== 1 ? {y:"ies", "":"s"}[end] : end return value !== 1 ? {y: "ies", "": "s"}[end] : end
} }
/**Formatters */ /**Formatters */
export function formatters({timeZone} = {}) { export function formatters({timeZone} = {}) {
//Check options //Check options
try { try {
new Date().toLocaleString("fr", {timeZoneName:"short", timeZone}) new Date().toLocaleString("fr", {timeZoneName: "short", timeZone})
} }
catch { catch {
timeZone = undefined timeZone = undefined
@@ -72,7 +72,7 @@ export function formatters({timeZone} = {}) {
/**Formatter */ /**Formatter */
const format = function(n, {sign = false, unit = true, fixed} = {}) { const format = function(n, {sign = false, unit = true, fixed} = {}) {
if (unit) { if (unit) {
for (const {u, v} of [{u:"b", v:10 ** 9}, {u:"m", v:10 ** 6}, {u:"k", v:10 ** 3}]) { for (const {u, v} of [{u: "b", v: 10 ** 9}, {u: "m", v: 10 ** 6}, {u: "k", v: 10 ** 3}]) {
if (n / v >= 1) if (n / v >= 1)
return `${(sign) && (n > 0) ? "+" : ""}${(n / v).toFixed(fixed ?? 2).substr(0, 4).replace(/[.]0*$/, "")}${u}` return `${(sign) && (n > 0) ? "+" : ""}${(n / v).toFixed(fixed ?? 2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
} }
@@ -82,7 +82,7 @@ export function formatters({timeZone} = {}) {
/**Bytes formatter */ /**Bytes formatter */
format.bytes = function(n) { format.bytes = function(n) {
for (const {u, v} of [{u:"E", v:10 ** 18}, {u:"P", v:10 ** 15}, {u:"T", v:10 ** 12}, {u:"G", v:10 ** 9}, {u:"M", v:10 ** 6}, {u:"k", v:10 ** 3}]) { for (const {u, v} of [{u: "E", v: 10 ** 18}, {u: "P", v: 10 ** 15}, {u: "T", v: 10 ** 12}, {u: "G", v: 10 ** 9}, {u: "M", v: 10 ** 6}, {u: "k", v: 10 ** 3}]) {
if (n / v >= 1) if (n / v >= 1)
return `${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B` return `${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B`
} }
@@ -110,11 +110,11 @@ export function formatters({timeZone} = {}) {
format.date = function(string, options) { format.date = function(string, options) {
if (options.date) { if (options.date) {
delete options.date delete options.date
Object.assign(options, {day:"numeric", month:"short", year:"numeric"}) Object.assign(options, {day: "numeric", month: "short", year: "numeric"})
} }
if (options.time) { if (options.time) {
delete options.time delete options.time
Object.assign(options, {hour:"2-digit", minute:"2-digit", second:"2-digit"}) Object.assign(options, {hour: "2-digit", minute: "2-digit", second: "2-digit"})
} }
return new Intl.DateTimeFormat("en-GB", {timeZone, ...options}).format(new Date(string)) return new Intl.DateTimeFormat("en-GB", {timeZone, ...options}).format(new Date(string))
} }
@@ -139,7 +139,7 @@ export function shuffle(array) {
} }
/**Escape html */ /**Escape html */
export function htmlescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) { export function htmlescape(string, u = {"&": true, "<": true, ">": true, '"': true, "'": true}) {
return string return string
.replace(/&(?!(?:amp|lt|gt|quot|apos);)/g, u["&"] ? "&amp;" : "&") .replace(/&(?!(?:amp|lt|gt|quot|apos);)/g, u["&"] ? "&amp;" : "&")
.replace(/</g, u["<"] ? "&lt;" : "<") .replace(/</g, u["<"] ? "&lt;" : "<")
@@ -149,7 +149,7 @@ export function htmlescape(string, u = {"&":true, "<":true, ">":true, '"':true,
} }
/**Unescape html */ /**Unescape html */
export function htmlunescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) { export function htmlunescape(string, u = {"&": true, "<": true, ">": true, '"': true, "'": true}) {
return string return string
.replace(/&lt;/g, u["<"] ? "<" : "&lt;") .replace(/&lt;/g, u["<"] ? "<" : "&lt;")
.replace(/&gt;/g, u[">"] ? ">" : "&gt;") .replace(/&gt;/g, u[">"] ? ">" : "&gt;")
@@ -173,7 +173,7 @@ export async function chartist() {
/**Language analyzer (single file) */ /**Language analyzer (single file) */
export async function language({filename, patch}) { export async function language({filename, patch}) {
console.debug(`metrics/language > ${filename}`) console.debug(`metrics/language > ${filename}`)
const {files:{results}} = await linguist(filename, {fileContent:patch}) const {files: {results}} = await linguist(filename, {fileContent: patch})
const result = (results[filename] ?? "unknown").toLocaleLowerCase() const result = (results[filename] ?? "unknown").toLocaleLowerCase()
console.debug(`metrics/language > ${filename} > result: ${result}`) console.debug(`metrics/language > ${filename} > result: ${result}`)
return result return result
@@ -181,7 +181,7 @@ export async function language({filename, patch}) {
/**Run command (use this to execute commands and process whole output at once, may not be suitable for large outputs) */ /**Run command (use this to execute commands and process whole output at once, may not be suitable for large outputs) */
export async function run(command, options, {prefixed = true, log = true} = {}) { export async function run(command, options, {prefixed = true, log = true} = {}) {
const prefix = {win32:"wsl"}[process.platform] ?? "" const prefix = {win32: "wsl"}[process.platform] ?? ""
command = `${prefixed ? prefix : ""} ${command}`.trim() command = `${prefixed ? prefix : ""} ${command}`.trim()
return new Promise((solve, reject) => { return new Promise((solve, reject) => {
console.debug(`metrics/command/run > ${command}`) console.debug(`metrics/command/run > ${command}`)
@@ -202,7 +202,7 @@ export async function run(command, options, {prefixed = true, log = true} = {})
/**Spawn command (use this to execute commands and process output on the fly) */ /**Spawn command (use this to execute commands and process output on the fly) */
export async function spawn(command, args = [], options = {}, {prefixed = true, timeout = 300 * 1000, stdout} = {}) { //eslint-disable-line max-params export async function spawn(command, args = [], options = {}, {prefixed = true, timeout = 300 * 1000, stdout} = {}) { //eslint-disable-line max-params
const prefix = {win32:"wsl"}[process.platform] ?? "" const prefix = {win32: "wsl"}[process.platform] ?? ""
if ((prefixed) && (prefix)) { if ((prefixed) && (prefix)) {
args.unshift(command) args.unshift(command)
command = prefix command = prefix
@@ -211,8 +211,8 @@ export async function spawn(command, args = [], options = {}, {prefixed = true,
throw new Error("`stdout` argument was not provided, use run() instead of spawn() if processing output is not needed") throw new Error("`stdout` argument was not provided, use run() instead of spawn() if processing output is not needed")
return new Promise((solve, reject) => { return new Promise((solve, reject) => {
console.debug(`metrics/command/spawn > ${command} with ${args.join(" ")}`) console.debug(`metrics/command/spawn > ${command} with ${args.join(" ")}`)
const child = processes.spawn(command, args, {...options, shell:true, timeout}) const child = processes.spawn(command, args, {...options, shell: true, timeout})
const reader = readline.createInterface({input:child.stdout}) const reader = readline.createInterface({input: child.stdout})
reader.on("line", stdout) reader.on("line", stdout)
const closed = new Promise(close => reader.on("close", close)) const closed = new Promise(close => reader.on("close", close))
child.on("close", async code => { child.on("close", async code => {
@@ -245,18 +245,18 @@ export function highlight(code, lang) {
/**Markdown-html sanitizer-interpreter */ /**Markdown-html sanitizer-interpreter */
export async function markdown(text, {mode = "inline", codelines = Infinity} = {}) { export async function markdown(text, {mode = "inline", codelines = Infinity} = {}) {
//Sanitize user input once to prevent injections and parse into markdown //Sanitize user input once to prevent injections and parse into markdown
let rendered = await marked.parse(htmlunescape(htmlsanitize(text)), {highlight, silent:true, xhtml:true}) let rendered = await marked.parse(htmlunescape(htmlsanitize(text)), {highlight, silent: true, xhtml: true})
//Markdown mode //Markdown mode
switch (mode) { switch (mode) {
case "inline": { case "inline": {
rendered = htmlsanitize( rendered = htmlsanitize(
htmlsanitize(rendered, { htmlsanitize(rendered, {
allowedTags:["h1", "h2", "h3", "h4", "h5", "h6", "br", "blockquote", "code", "span"], allowedTags: ["h1", "h2", "h3", "h4", "h5", "h6", "br", "blockquote", "code", "span"],
allowedAttributes:{code:["class"], span:["class"]}, allowedAttributes: {code: ["class"], span: ["class"]},
}), }),
{ {
allowedAttributes:{code:["class"], span:["class"]}, allowedAttributes: {code: ["class"], span: ["class"]},
transformTags:{h1:"b", h2:"b", h3:"b", h4:"b", h5:"b", h6:"b", blockquote:"i"}, transformTags: {h1: "b", h2: "b", h3: "b", h4: "b", h5: "b", h6: "b", blockquote: "i"},
}, },
) )
break break
@@ -349,7 +349,7 @@ export const svg = {
} }
//Additional transformations //Additional transformations
if (twemojis) if (twemojis)
rendered = await svg.twemojis(rendered, {custom:false}) rendered = await svg.twemojis(rendered, {custom: false})
if ((gemojis) && (rest)) if ((gemojis) && (rest))
rendered = await svg.gemojis(rendered, {rest}) rendered = await svg.gemojis(rendered, {rest})
if (octicons) if (octicons)
@@ -358,13 +358,13 @@ export const svg = {
//Render through browser and print pdf //Render through browser and print pdf
console.debug("metrics/svg/pdf > loading svg") console.debug("metrics/svg/pdf > loading svg")
const page = await svg.resize.browser.newPage() const page = await svg.resize.browser.newPage()
page.on("console", ({_text:text}) => console.debug(`metrics/svg/pdf > puppeteer > ${text}`)) page.on("console", ({_text: text}) => console.debug(`metrics/svg/pdf > puppeteer > ${text}`))
await page.setContent(`<main class="markdown-body">${rendered}</main>`, {waitUntil:["load", "domcontentloaded", "networkidle2"]}) await page.setContent(`<main class="markdown-body">${rendered}</main>`, {waitUntil: ["load", "domcontentloaded", "networkidle2"]})
console.debug("metrics/svg/pdf > loaded svg successfully") console.debug("metrics/svg/pdf > loaded svg successfully")
const margins = (Array.isArray(paddings) ? paddings : paddings.split(",")).join(" ") const margins = (Array.isArray(paddings) ? paddings : paddings.split(",")).join(" ")
console.debug(`metrics/svg/pdf > margins set to ${margins}`) console.debug(`metrics/svg/pdf > margins set to ${margins}`)
await page.addStyleTag({ await page.addStyleTag({
content:` content: `
main { margin: ${margins}; } main { margin: ${margins}; }
main svg { height: 1em; width: 1em; } main svg { height: 1em; width: 1em; }
${await fs.readFile(paths.join(__module(import.meta.url), "../../../node_modules", "@primer/css/dist/markdown.css")).catch(_ => "")}${style} ${await fs.readFile(paths.join(__module(import.meta.url), "../../../node_modules", "@primer/css/dist/markdown.css")).catch(_ => "")}${style}
@@ -374,7 +374,7 @@ export const svg = {
//Result //Result
await page.close() await page.close()
console.debug("metrics/svg/pdf > rendering complete") console.debug("metrics/svg/pdf > rendering complete")
return {rendered, mime:"application/pdf"} return {rendered, mime: "application/pdf"}
}, },
/**Render and resize svg */ /**Render and resize svg */
async resize(rendered, {paddings, convert, scripts = []}) { async resize(rendered, {paddings, convert, scripts = []}) {
@@ -384,7 +384,7 @@ export const svg = {
console.debug(`metrics/svg/resize > started ${await svg.resize.browser.version()}`) console.debug(`metrics/svg/resize > started ${await svg.resize.browser.version()}`)
} }
//Format padding //Format padding
const padding = {width:1, height:1, absolute:{width:0, height:0}} const padding = {width: 1, height: 1, absolute: {width: 0, height: 0}}
paddings = Array.isArray(paddings) ? paddings : `${paddings}`.split(",").map(x => x.trim()) paddings = Array.isArray(paddings) ? paddings : `${paddings}`.split(",").map(x => x.trim())
for (const [i, dimension] of [[0, "width"], [1, "height"]]) { for (const [i, dimension] of [[0, "width"], [1, "height"]]) {
let operands = (paddings?.[i] ?? paddings[0]) let operands = (paddings?.[i] ?? paddings[0])
@@ -400,18 +400,18 @@ export const svg = {
//Render through browser and resize height //Render through browser and resize height
console.debug("metrics/svg/resize > loading svg") console.debug("metrics/svg/resize > loading svg")
const page = await svg.resize.browser.newPage() const page = await svg.resize.browser.newPage()
page.setViewport({width:980, height:980}) page.setViewport({width: 980, height: 980})
page page
.on("console", message => console.debug(`metrics/svg/resize > puppeteer > ${message.text()}`)) .on("console", message => console.debug(`metrics/svg/resize > puppeteer > ${message.text()}`))
.on("pageerror", error => console.debug(`metrics/svg/resize > puppeteer > ${error.message}`)) .on("pageerror", error => console.debug(`metrics/svg/resize > puppeteer > ${error.message}`))
await page.setContent(rendered, {waitUntil:["load", "domcontentloaded", "networkidle2"]}) await page.setContent(rendered, {waitUntil: ["load", "domcontentloaded", "networkidle2"]})
console.debug("metrics/svg/resize > loaded svg successfully") console.debug("metrics/svg/resize > loaded svg successfully")
await page.addStyleTag({content:"body { margin: 0; padding: 0; }"}) await page.addStyleTag({content: "body { margin: 0; padding: 0; }"})
let mime = "image/svg+xml" let mime = "image/svg+xml"
console.debug("metrics/svg/resize > resizing svg") console.debug("metrics/svg/resize > resizing svg")
let height, resized, width let height, resized, width
try { try {
({resized, width, height} = await page.evaluate( ;({resized, width, height} = await page.evaluate(
async (padding, scripts) => { async (padding, scripts) => {
//Execute additional JavaScript //Execute additional JavaScript
for (const script of scripts) { for (const script of scripts) {
@@ -431,7 +431,7 @@ export const svg = {
console.debug(`animations are ${animated ? "enabled" : "disabled"}`) console.debug(`animations are ${animated ? "enabled" : "disabled"}`)
await new Promise(solve => setTimeout(solve, 2400)) await new Promise(solve => setTimeout(solve, 2400))
//Get bounds and resize //Get bounds and resize
let {y:height, width} = document.querySelector("svg #metrics-end").getBoundingClientRect() let {y: height, width} = document.querySelector("svg #metrics-end").getBoundingClientRect()
console.debug(`bounds width=${width}, height=${height}`) console.debug(`bounds width=${width}, height=${height}`)
height = Math.max(1, Math.ceil(height * padding.height + padding.absolute.height)) height = Math.max(1, Math.ceil(height * padding.height + padding.absolute.height))
width = Math.max(1, Math.ceil(width * padding.width + padding.absolute.width)) width = Math.max(1, Math.ceil(width * padding.width + padding.absolute.width))
@@ -445,7 +445,7 @@ export const svg = {
if (animated) if (animated)
document.querySelector("svg").classList.remove("no-animations") document.querySelector("svg").classList.remove("no-animations")
//Result //Result
return {resized:new XMLSerializer().serializeToString(document.querySelector("svg")), height, width} return {resized: new XMLSerializer().serializeToString(document.querySelector("svg")), height, width}
}, },
padding, padding,
scripts, scripts,
@@ -458,7 +458,7 @@ export const svg = {
//Convert if required //Convert if required
if (convert) { if (convert) {
console.debug(`metrics/svg/resize > convert to ${convert}`) console.debug(`metrics/svg/resize > convert to ${convert}`)
resized = await page.screenshot({type:convert, clip:{x:0, y:0, width, height}, omitBackground:true}) resized = await page.screenshot({type: convert, clip: {x: 0, y: 0, width, height}, omitBackground: true})
mime = `image/${convert}` mime = `image/${convert}`
} }
//Result //Result
@@ -478,7 +478,7 @@ export const svg = {
} }
//Compute hash //Compute hash
const page = await svg.resize.browser.newPage() const page = await svg.resize.browser.newPage()
await page.setContent(rendered, {waitUntil:["load", "domcontentloaded", "networkidle2"]}) await page.setContent(rendered, {waitUntil: ["load", "domcontentloaded", "networkidle2"]})
const data = await page.evaluate(async () => { const data = await page.evaluate(async () => {
document.querySelector("footer")?.remove() document.querySelector("footer")?.remove()
return document.querySelector("svg").outerHTML return document.querySelector("svg").outerHTML
@@ -494,7 +494,7 @@ export const svg = {
//Load emojis //Load emojis
console.debug("metrics/svg/twemojis > rendering twemojis") console.debug("metrics/svg/twemojis > rendering twemojis")
const emojis = new Map() const emojis = new Map()
for (const {text:emoji, url} of twemojis.parse(rendered)) { for (const {text: emoji, url} of twemojis.parse(rendered)) {
if (!emojis.has(emoji)) if (!emojis.has(emoji))
emojis.set(emoji, (await axios.get(url)).data.replace(/^<svg /, '<svg class="twemoji" ')) emojis.set(emoji, (await axios.get(url)).data.replace(/^<svg /, '<svg class="twemoji" '))
} }
@@ -513,7 +513,7 @@ export const svg = {
const emojis = new Map() const emojis = new Map()
try { try {
for (const [emoji, url] of Object.entries((await rest.emojis.get()).data).map(([key, value]) => [`:${key}:`, value])) { for (const [emoji, url] of Object.entries((await rest.emojis.get()).data).map(([key, value]) => [`:${key}:`, value])) {
if (((!emojis.has(emoji))) && (new RegExp(emoji, "g").test(rendered))) if ((!emojis.has(emoji)) && (new RegExp(emoji, "g").test(rendered)))
emojis.set(emoji, `<img class="gemoji" src="${await imgb64(url)}" height="16" width="16" alt="" />`) emojis.set(emoji, `<img class="gemoji" src="${await imgb64(url)}" height="16" width="16" alt="" />`)
} }
} }
@@ -535,9 +535,9 @@ export const svg = {
for (const size of Object.keys(heights)) { for (const size of Object.keys(heights)) {
const octicon = `:octicon-${name}-${size}:` const octicon = `:octicon-${name}-${size}:`
if (new RegExp(`:octicon-${name}(?:-[0-9]+)?:`, "g").test(rendered)) { if (new RegExp(`:octicon-${name}(?:-[0-9]+)?:`, "g").test(rendered)) {
icons.set(octicon, toSVG({height:size, width:size})) icons.set(octicon, toSVG({height: size, width: size}))
if (Number(size) === 16) if (Number(size) === 16)
icons.set(`:octicon-${name}:`, toSVG({height:size, width:size})) icons.set(`:octicon-${name}:`, toSVG({height: size, width: size}))
} }
} }
} }
@@ -547,7 +547,7 @@ export const svg = {
return rendered return rendered
}, },
/**Optimizers */ /**Optimizers */
optimize:{ optimize: {
/**CSS optimizer */ /**CSS optimizer */
async css(rendered) { async css(rendered) {
//Extract styles //Extract styles
@@ -558,9 +558,9 @@ export const svg = {
while (regex.test(rendered)) { while (regex.test(rendered)) {
const style = htmlunescape(rendered.match(regex)?.groups?.style ?? "") const style = htmlunescape(rendered.match(regex)?.groups?.style ?? "")
rendered = rendered.replace(regex, cleaned) rendered = rendered.replace(regex, cleaned)
css.push({raw:style}) css.push({raw: style})
} }
const content = [{raw:rendered, extension:"html"}] const content = [{raw: rendered, extension: "html"}]
//Purge CSS //Purge CSS
const purged = await new purgecss.PurgeCSS().purge({content, css}) const purged = await new purgecss.PurgeCSS().purge({content, css})
@@ -574,7 +574,7 @@ export const svg = {
console.debug("metrics/svg/optimize/xml > skipped as raw option is enabled") console.debug("metrics/svg/optimize/xml > skipped as raw option is enabled")
return rendered return rendered
} }
return xmlformat(rendered, {lineSeparator:"\n", collapseContent:true}) return xmlformat(rendered, {lineSeparator: "\n", collapseContent: true})
}, },
/**SVG optimizer */ /**SVG optimizer */
async svg(rendered, {raw = false} = {}, experimental = new Set()) { async svg(rendered, {raw = false} = {}, experimental = new Set()) {
@@ -587,16 +587,16 @@ export const svg = {
console.debug("metrics/svg/optimize/svg > this feature require experimental feature flag --optimize-svg") console.debug("metrics/svg/optimize/svg > this feature require experimental feature flag --optimize-svg")
return rendered return rendered
} }
const {error, data:optimized} = await SVGO.optimize(rendered, { const {error, data: optimized} = await SVGO.optimize(rendered, {
multipass:true, multipass: true,
plugins:SVGO.extendDefaultPlugins([ plugins: SVGO.extendDefaultPlugins([
//Additional cleanup //Additional cleanup
{name:"cleanupListOfValues"}, {name: "cleanupListOfValues"},
{name:"removeRasterImages"}, {name: "removeRasterImages"},
{name:"removeScriptElement"}, {name: "removeScriptElement"},
//Force CSS style consistency //Force CSS style consistency
{name:"inlineStyles", active:false}, {name: "inlineStyles", active: false},
{name:"removeViewBox", active:false}, {name: "removeViewBox", active: false},
]), ]),
}) })
if (error) if (error)
@@ -616,7 +616,7 @@ export async function record({page, width, height, frames, scale = 1, quality =
//Register images frames //Register images frames
const images = [] const images = []
for (let i = 0; i < frames; i++) { for (let i = 0; i < frames; i++) {
images.push(await page.screenshot({type:"png", clip:{width, height, x, y}, omitBackground:background})) images.push(await page.screenshot({type: "png", clip: {width, height, x, y}, omitBackground: background}))
await wait(delay / 1000) await wait(delay / 1000)
if (i % 10 === 0) if (i % 10 === 0)
console.debug(`metrics/record > processed ${i}/${frames} frames`) console.debug(`metrics/record > processed ${i}/${frames} frames`)
@@ -643,7 +643,7 @@ export async function gif({page, width, height, frames, x = 0, y = 0, repeat = t
encoder.setQuality(quality) encoder.setQuality(quality)
//Register frames //Register frames
for (let i = 0; i < frames; i++) { for (let i = 0; i < frames; i++) {
const buffer = new PNG(await page.screenshot({clip:{width, height, x, y}})) const buffer = new PNG(await page.screenshot({clip: {width, height, x, y}}))
encoder.addFrame(await new Promise(solve => buffer.decode(pixels => solve(pixels)))) encoder.addFrame(await new Promise(solve => buffer.decode(pixels => solve(pixels))))
if (frames % 10 === 0) if (frames % 10 === 0)
console.debug(`metrics/puppeteergif > processed ${i}/${frames} frames`) console.debug(`metrics/puppeteergif > processed ${i}/${frames} frames`)

View File

@@ -1,4 +1,4 @@
import app from "./instance.mjs" import app from "./instance.mjs"
;(async function() { ;(async function() {
await app({sandbox:process.env.SANDBOX}) await app({sandbox: process.env.SANDBOX})
})() })()

View File

@@ -18,7 +18,7 @@ export default async function({sandbox = false} = {}) {
//Sandbox mode //Sandbox mode
if (sandbox) { if (sandbox) {
console.debug("metrics/app > sandbox mode is specified, enabling advanced features") console.debug("metrics/app > sandbox mode is specified, enabling advanced features")
Object.assign(conf.settings, {sandbox:true, optimize:true, cached:0, "plugins.default":true, extras:{default:true}}) Object.assign(conf.settings, {sandbox: true, optimize: true, cached: 0, "plugins.default": true, extras: {default: true}})
} }
const {token, maxusers = 0, restricted = [], debug = false, cached = 30 * 60 * 1000, port = 3000, ratelimiter = null, plugins = null} = conf.settings const {token, maxusers = 0, restricted = [], debug = false, cached = 30 * 60 * 1000, port = 3000, ratelimiter = null, plugins = null} = conf.settings
const mock = sandbox || conf.settings.mocked const mock = sandbox || conf.settings.mocked
@@ -48,10 +48,10 @@ export default async function({sandbox = false} = {}) {
conf.settings.token = "MOCKED_TOKEN" conf.settings.token = "MOCKED_TOKEN"
} }
if (debug) if (debug)
console.debug(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256})) console.debug(util.inspect(conf.settings, {depth: Infinity, maxStringLength: 256}))
//Load octokits //Load octokits
const api = {graphql:octokit.graphql.defaults({headers:{authorization:`token ${token}`}}), rest:new OctokitRest.Octokit({auth:token})} const api = {graphql: octokit.graphql.defaults({headers: {authorization: `token ${token}`}}), rest: new OctokitRest.Octokit({auth: token})}
//Apply mocking if needed //Apply mocking if needed
if (mock) if (mock)
Object.assign(api, await mocks(api)) Object.assign(api, await mocks(api))
@@ -68,8 +68,8 @@ export default async function({sandbox = false} = {}) {
skip(req, _res) { skip(req, _res) {
return !!cache.get(req.params.login) return !!cache.get(req.params.login)
}, },
message:"Too many requests: retry later", message: "Too many requests: retry later",
headers:true, headers: true,
...ratelimiter, ...ratelimiter,
})) }))
} }
@@ -84,24 +84,24 @@ export default async function({sandbox = false} = {}) {
}) })
//Base routes //Base routes
const limiter = ratelimit({max:debug ? Number.MAX_SAFE_INTEGER : 60, windowMs:60 * 1000, headers:false}) const limiter = ratelimit({max: debug ? Number.MAX_SAFE_INTEGER : 60, windowMs: 60 * 1000, headers: false})
const metadata = Object.fromEntries( const metadata = Object.fromEntries(
Object.entries(conf.metadata.plugins) Object.entries(conf.metadata.plugins)
.map(([key, value]) => [key, Object.fromEntries(Object.entries(value).filter(([key]) => ["name", "icon", "category", "web", "supports", "scopes"].includes(key)))]) .map(([key, value]) => [key, Object.fromEntries(Object.entries(value).filter(([key]) => ["name", "icon", "category", "web", "supports", "scopes"].includes(key)))])
.map(([key, value]) => [key, key === "core" ? {...value, web:Object.fromEntries(Object.entries(value.web).filter(([key]) => /^config[.]/.test(key)).map(([key, value]) => [key.replace(/^config[.]/, ""), value]))} : value]), .map(([key, value]) => [key, key === "core" ? {...value, web: Object.fromEntries(Object.entries(value.web).filter(([key]) => /^config[.]/.test(key)).map(([key, value]) => [key.replace(/^config[.]/, ""), value]))} : value]),
) )
const enabled = Object.entries(metadata).filter(([_name, {category}]) => category !== "core").map(([name]) => ({name, category:metadata[name]?.category ?? "community", enabled:plugins[name]?.enabled ?? false})) const enabled = Object.entries(metadata).filter(([_name, {category}]) => category !== "core").map(([name]) => ({name, category: metadata[name]?.category ?? "community", enabled: plugins[name]?.enabled ?? false}))
const templates = Object.entries(Templates).map(([name]) => ({name, enabled:(conf.settings.templates.enabled.length ? conf.settings.templates.enabled.includes(name) : true) ?? false})) const templates = Object.entries(Templates).map(([name]) => ({name, enabled: (conf.settings.templates.enabled.length ? conf.settings.templates.enabled.includes(name) : true) ?? false}))
const actions = {flush:new Map()} const actions = {flush: new Map()}
const requests = {rest:{limit:0, used:0, remaining:0, reset:NaN}, graphql:{limit:0, used:0, remaining:0, reset:NaN}} const requests = {rest: {limit: 0, used: 0, remaining: 0, reset: NaN}, graphql: {limit: 0, used: 0, remaining: 0, reset: NaN}}
let _requests_refresh = false let _requests_refresh = false
if (!conf.settings.notoken) { if (!conf.settings.notoken) {
const refresh = async () => { const refresh = async () => {
try { try {
const {limit} = await graphql("{ limit:rateLimit {limit remaining reset:resetAt used} }") const {limit} = await graphql("{ limit:rateLimit {limit remaining reset:resetAt used} }")
Object.assign(requests, { Object.assign(requests, {
rest:(await rest.rateLimit.get()).data.rate, rest: (await rest.rateLimit.get()).data.rate,
graphql:{...limit, reset:new Date(limit.reset).getTime()}, graphql: {...limit, reset: new Date(limit.reset).getTime()},
}) })
} }
catch { catch {
@@ -245,9 +245,9 @@ export default async function({sandbox = false} = {}) {
console.debug(`metrics/app/${login} > awaiting pending request`) console.debug(`metrics/app/${login} > awaiting pending request`)
await pending.get(login) await pending.get(login)
} }
else else {
pending.set(login, new Promise(_solve => solve = _solve)) pending.set(login, new Promise(_solve => solve = _solve))
}
//Read cached data if possible //Read cached data if possible
if ((!debug) && (cached) && (cache.get(login))) { if ((!debug) && (cached) && (cache.get(login))) {
@@ -273,7 +273,7 @@ export default async function({sandbox = false} = {}) {
try { try {
//Render //Render
const q = req.query const q = req.query
console.debug(`metrics/app/${login} > ${util.inspect(q, {depth:Infinity, maxStringLength:256})}`) console.debug(`metrics/app/${login} > ${util.inspect(q, {depth: Infinity, maxStringLength: 256})}`)
if ((q["config.presets"]) && (conf.settings.extras?.presets ?? conf.settings.extras?.default ?? false)) { if ((q["config.presets"]) && (conf.settings.extras?.presets ?? conf.settings.extras?.default ?? false)) {
console.debug(`metrics/app/${login} > presets have been specified, loading them`) console.debug(`metrics/app/${login} > presets have been specified, loading them`)
Object.assign(q, await presets(q["config.presets"])) Object.assign(q, await presets(q["config.presets"]))
@@ -283,9 +283,9 @@ export default async function({sandbox = false} = {}) {
rest, rest,
plugins, plugins,
conf, conf,
die:q["plugins.errors.fatal"] ?? false, die: q["plugins.errors.fatal"] ?? false,
verify:q.verify ?? false, verify: q.verify ?? false,
convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(q["config.output"]) ? q["config.output"] : null, convert: ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf", "insights"].includes(q["config.output"]) ? q["config.output"] : null,
}, {Plugins, Templates}) }, {Plugins, Templates})
//Cache //Cache
if ((!debug) && (cached)) { if ((!debug) && (cached)) {
@@ -331,13 +331,14 @@ export default async function({sandbox = false} = {}) {
}) })
//Listen //Listen
app.listen(port, () => console.log([ app.listen(port, () =>
console.log([
`Listening on port │ ${port}`, `Listening on port │ ${port}`,
`Debug mode │ ${debug}`, `Debug mode │ ${debug}`,
`Mocked data │ ${conf.settings.mocked ?? false}`, `Mocked data │ ${conf.settings.mocked ?? false}`,
`Restricted to users │ ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`, `Restricted to users │ ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
`Cached time │ ${cached} seconds`, `Cached time │ ${cached} seconds`,
`Rate limiter │ ${ratelimiter ? util.inspect(ratelimiter, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`, `Rate limiter │ ${ratelimiter ? util.inspect(ratelimiter, {depth: Infinity, maxStringLength: 256}) : "(enabled)"}`,
`Max simultaneous users │ ${maxusers ? `${maxusers} users` : "(unrestricted)"}`, `Max simultaneous users │ ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
`Plugins enabled │ ${enabled.map(({name}) => name).join(", ")}`, `Plugins enabled │ ${enabled.map(({name}) => name).join(", ")}`,
`SVG optimization │ ${conf.settings.optimize ?? false}`, `SVG optimization │ ${conf.settings.optimize ?? false}`,

View File

@@ -6,7 +6,7 @@
async mounted() { async mounted() {
//Palette //Palette
try { try {
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") this.palette = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
} }
catch (error) {} catch (error) {}
//Embed //Embed
@@ -26,17 +26,17 @@
await Promise.all([ await Promise.all([
//GitHub limit tracker //GitHub limit tracker
(async () => { (async () => {
const { data: requests } = await axios.get("/.requests") const {data: requests} = await axios.get("/.requests")
this.requests = requests this.requests = requests
})(), })(),
//Version //Version
(async () => { (async () => {
const { data: version } = await axios.get("/.version") const {data: version} = await axios.get("/.version")
this.version = `v${version}` this.version = `v${version}`
})(), })(),
//Hosted //Hosted
(async () => { (async () => {
const { data: hosted } = await axios.get("/.hosted") const {data: hosted} = await axios.get("/.hosted")
this.hosted = hosted this.hosted = hosted
})(), })(),
]) ])
@@ -89,12 +89,12 @@
this.metrics = (await axios.get(`/about/query/${this.user}`)).data this.metrics = (await axios.get(`/about/query/${this.user}`)).data
} }
catch (error) { catch (error) {
this.error = { code: error.response.status, message: error.response.data } this.error = {code: error.response.status, message: error.response.data}
} }
finally { finally {
this.pending = false this.pending = false
try { try {
const { data: requests } = await axios.get("/.requests") const {data: requests} = await axios.get("/.requests")
this.requests = requests this.requests = requests
} }
catch {} catch {}
@@ -104,10 +104,10 @@
//Computed properties //Computed properties
computed: { computed: {
ranked() { ranked() {
return this.metrics?.rendered.plugins.achievements.list?.filter(({ leaderboard }) => leaderboard).sort((a, b) => a.leaderboard.type.localeCompare(b.leaderboard.type)) ?? [] return this.metrics?.rendered.plugins.achievements.list?.filter(({leaderboard}) => leaderboard).sort((a, b) => a.leaderboard.type.localeCompare(b.leaderboard.type)) ?? []
}, },
achievements() { achievements() {
return this.metrics?.rendered.plugins.achievements.list?.filter(({ leaderboard }) => !leaderboard).filter(({ title }) => !/(?:automator|octonaut|infographile)/i.test(title)) ?? [] return this.metrics?.rendered.plugins.achievements.list?.filter(({leaderboard}) => !leaderboard).filter(({title}) => !/(?:automator|octonaut|infographile)/i.test(title)) ?? []
}, },
introduction() { introduction() {
return this.metrics?.rendered.plugins.introduction?.text ?? "" return this.metrics?.rendered.plugins.introduction?.text ?? ""
@@ -138,8 +138,8 @@
account() { account() {
if (!this.metrics) if (!this.metrics)
return null return null
const { login, name } = this.metrics.rendered.user const {login, name} = this.metrics.rendered.user
return { login, name, avatar: this.metrics.rendered.computed.avatar, type: this.metrics?.rendered.account } return {login, name, avatar: this.metrics.rendered.computed.avatar, type: this.metrics?.rendered.account}
}, },
url() { url() {
return `${window.location.protocol}//${window.location.host}/about/${this.user}` return `${window.location.protocol}//${window.location.host}/about/${this.user}`
@@ -160,7 +160,7 @@
embed: false, embed: false,
localstorage: false, localstorage: false,
searchable: false, searchable: false,
requests: { rest: { limit: 0, used: 0, remaining: 0, reset: NaN }, graphql: { limit: 0, used: 0, remaining: 0, reset: NaN } }, requests: {rest: {limit: 0, used: 0, remaining: 0, reset: NaN}, graphql: {limit: 0, used: 0, remaining: 0, reset: NaN}},
palette: "light", palette: "light",
metrics: null, metrics: null,
pending: false, pending: false,

View File

@@ -1,6 +1,6 @@
;(async function() { ;(async function() {
//Init //Init
const { data: metadata } = await axios.get("/.plugins.metadata") const {data: metadata} = await axios.get("/.plugins.metadata")
delete metadata.core.web.output delete metadata.core.web.output
delete metadata.core.web.twemojis delete metadata.core.web.twemojis
//App //App
@@ -11,49 +11,49 @@
//Interpolate config from browser //Interpolate config from browser
try { try {
this.config.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone this.config.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") this.palette = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
} }
catch (error) {} catch (error) {}
//Init //Init
await Promise.all([ await Promise.all([
//GitHub limit tracker //GitHub limit tracker
(async () => { (async () => {
const { data: requests } = await axios.get("/.requests") const {data: requests} = await axios.get("/.requests")
this.requests = requests this.requests = requests
})(), })(),
//Templates //Templates
(async () => { (async () => {
const { data: templates } = await axios.get("/.templates") const {data: templates} = await axios.get("/.templates")
templates.sort((a, b) => (a.name.startsWith("@") ^ b.name.startsWith("@")) ? (a.name.startsWith("@") ? 1 : -1) : a.name.localeCompare(b.name)) templates.sort((a, b) => (a.name.startsWith("@") ^ b.name.startsWith("@")) ? (a.name.startsWith("@") ? 1 : -1) : a.name.localeCompare(b.name))
this.templates.list = templates this.templates.list = templates
this.templates.selected = templates[0]?.name || "classic" this.templates.selected = templates[0]?.name || "classic"
})(), })(),
//Plugins //Plugins
(async () => { (async () => {
const { data: plugins } = await axios.get("/.plugins") const {data: plugins} = await axios.get("/.plugins")
this.plugins.list = plugins.filter(({ name }) => metadata[name]?.supports.includes("user") || metadata[name]?.supports.includes("organization")) this.plugins.list = plugins.filter(({name}) => metadata[name]?.supports.includes("user") || metadata[name]?.supports.includes("organization"))
const categories = [...new Set(this.plugins.list.map(({ category }) => category))] const categories = [...new Set(this.plugins.list.map(({category}) => category))]
this.plugins.categories = Object.fromEntries(categories.map(category => [category, this.plugins.list.filter(value => category === value.category)])) this.plugins.categories = Object.fromEntries(categories.map(category => [category, this.plugins.list.filter(value => category === value.category)]))
})(), })(),
//Base //Base
(async () => { (async () => {
const { data: base } = await axios.get("/.plugins.base") const {data: base} = await axios.get("/.plugins.base")
this.plugins.base = base this.plugins.base = base
this.plugins.enabled.base = Object.fromEntries(base.map(key => [key, true])) this.plugins.enabled.base = Object.fromEntries(base.map(key => [key, true]))
})(), })(),
//Version //Version
(async () => { (async () => {
const { data: version } = await axios.get("/.version") const {data: version} = await axios.get("/.version")
this.version = `v${version}` this.version = `v${version}`
})(), })(),
//Hosted //Hosted
(async () => { (async () => {
const { data: hosted } = await axios.get("/.hosted") const {data: hosted} = await axios.get("/.hosted")
this.hosted = hosted this.hosted = hosted
})(), })(),
]) ])
//Generate placeholder //Generate placeholder
this.mock({ timeout: 200 }) this.mock({timeout: 200})
setInterval(() => { setInterval(() => {
const marker = document.querySelector("#metrics-end") const marker = document.querySelector("#metrics-end")
if (marker) { if (marker) {
@@ -62,7 +62,7 @@
} }
}, 100) }, 100)
}, },
components: { Prism: PrismComponent }, components: {Prism: PrismComponent},
//Watchers //Watchers
watch: { watch: {
tab: { tab: {
@@ -90,10 +90,10 @@
tab: "overview", tab: "overview",
palette: "light", palette: "light",
clipboard: null, clipboard: null,
requests: { rest: { limit: 0, used: 0, remaining: 0, reset: NaN }, graphql: { limit: 0, used: 0, remaining: 0, reset: NaN } }, requests: {rest: {limit: 0, used: 0, remaining: 0, reset: NaN}, graphql: {limit: 0, used: 0, remaining: 0, reset: NaN}},
cached: new Map(), cached: new Map(),
config: Object.fromEntries(Object.entries(metadata.core.web).map(([key, { defaulted }]) => [key, defaulted])), config: Object.fromEntries(Object.entries(metadata.core.web).map(([key, {defaulted}]) => [key, defaulted])),
metadata: Object.fromEntries(Object.entries(metadata).map(([key, { web }]) => [key, web])), metadata: Object.fromEntries(Object.entries(metadata).map(([key, {web}]) => [key, web])),
hosted: null, hosted: null,
docs: { docs: {
overview: { overview: {
@@ -121,15 +121,15 @@
"base.community": "Community stats", "base.community": "Community stats",
"base.repositories": "Repositories metrics", "base.repositories": "Repositories metrics",
"base.metadata": "Metadata", "base.metadata": "Metadata",
...Object.fromEntries(Object.entries(metadata).map(([key, { name }]) => [key, name])), ...Object.fromEntries(Object.entries(metadata).map(([key, {name}]) => [key, name])),
}, },
options: { options: {
descriptions: { ...(Object.assign({}, ...Object.entries(metadata).flatMap(([key, { web }]) => web))) }, descriptions: {...(Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)))},
...(Object.fromEntries( ...(Object.fromEntries(
Object.entries( Object.entries(
Object.assign({}, ...Object.entries(metadata).flatMap(([key, { web }]) => web)), Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)),
) )
.map(([key, { defaulted }]) => [key, defaulted]), .map(([key, {defaulted}]) => [key, defaulted]),
)), )),
}, },
}, },
@@ -157,7 +157,7 @@
computed: { computed: {
//Unusable plugins //Unusable plugins
unusable() { unusable() {
return this.plugins.list.filter(({ name }) => this.plugins.enabled[name]).filter(({ enabled }) => !enabled).map(({ name }) => name) return this.plugins.list.filter(({name}) => this.plugins.enabled[name]).filter(({enabled}) => !enabled).map(({name}) => name)
}, },
//User's avatar //User's avatar
avatar() { avatar() {
@@ -239,13 +239,13 @@
` base: ${Object.entries(this.plugins.enabled.base).filter(([key, value]) => value).map(([key]) => key).join(", ") || '""'}`, ` base: ${Object.entries(this.plugins.enabled.base).filter(([key, value]) => value).map(([key]) => key).join(", ") || '""'}`,
...[ ...[
...Object.entries(this.plugins.options).filter(([key, value]) => (key in metadata.base.web) && (value !== metadata.base.web[key]?.defaulted)).map(([key, value]) => ...Object.entries(this.plugins.options).filter(([key, value]) => (key in metadata.base.web) && (value !== metadata.base.web[key]?.defaulted)).map(([key, value]) =>
` ${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? { true: "yes", false: "no" }[value] : value}` ` ${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`
), ),
...Object.entries(this.plugins.enabled).filter(([key, value]) => (key !== "base") && (value)).map(([key]) => ` plugin_${key}: yes`), ...Object.entries(this.plugins.enabled).filter(([key, value]) => (key !== "base") && (value)).map(([key]) => ` plugin_${key}: yes`),
...Object.entries(this.plugins.options).filter(([key, value]) => value).filter(([key, value]) => this.plugins.enabled[key.split(".")[0]]).map(([key, value]) => ...Object.entries(this.plugins.options).filter(([key, value]) => value).filter(([key, value]) => this.plugins.enabled[key.split(".")[0]]).map(([key, value]) =>
` plugin_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? { true: "yes", false: "no" }[value] : value}` ` plugin_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`
), ),
...Object.entries(this.config).filter(([key, value]) => (value) && (value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => ` config_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? { true: "yes", false: "no" }[value] : value}`), ...Object.entries(this.config).filter(([key, value]) => (value) && (value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => ` config_${key.replace(/[.]/g, "_")}: ${typeof value === "boolean" ? {true: "yes", false: "no"}[value] : value}`),
].sort(), ].sort(),
].join("\n") ].join("\n")
}, },
@@ -276,7 +276,7 @@
methods: { methods: {
//Refresh computed properties //Refresh computed properties
async refresh() { async refresh() {
const keys = { action: ["scopes", "action"], markdown: ["url", "embed"] }[this.tab] const keys = {action: ["scopes", "action"], markdown: ["url", "embed"]}[this.tab]
if (keys) { if (keys) {
for (const key of keys) for (const key of keys)
this._computedWatchers[key]?.run() this._computedWatchers[key]?.run()
@@ -284,7 +284,7 @@
} }
}, },
//Load and render placeholder image //Load and render placeholder image
async mock({ timeout = 600 } = {}) { async mock({timeout = 600} = {}) {
this.refresh() this.refresh()
clearTimeout(this.templates.placeholder.timeout) clearTimeout(this.templates.placeholder.timeout)
this.templates.placeholder.timeout = setTimeout(async () => { this.templates.placeholder.timeout = setTimeout(async () => {
@@ -315,12 +315,12 @@
this.generated.error = null this.generated.error = null
} }
catch (error) { catch (error) {
this.generated.error = { code: error.response.status, message: error.response.data } this.generated.error = {code: error.response.status, message: error.response.data}
} }
finally { finally {
this.generated.pending = false this.generated.pending = false
try { try {
const { data: requests } = await axios.get("/.requests") const {data: requests} = await axios.get("/.requests")
this.requests = requests this.requests = requests
} }
catch {} catch {}

View File

@@ -1,4 +1,4 @@
;(function({ axios, faker, ejs } = { axios: globalThis.axios, faker: globalThis.faker, ejs: globalThis.ejs }) { ;(function({axios, faker, ejs} = {axios: globalThis.axios, faker: globalThis.faker, ejs: globalThis.ejs}) {
//Load assets //Load assets
const cached = new Map() const cached = new Map()
async function load(url) { async function load(url) {
@@ -31,7 +31,7 @@
//Placeholder function //Placeholder function
globalThis.placeholder = async function(set) { globalThis.placeholder = async function(set) {
//Load templates informations //Load templates informations
let { image, style, fonts, partials } = await load(`/.templates/${set.templates.selected}`) let {image, style, fonts, partials} = await load(`/.templates/${set.templates.selected}`)
await Promise.all(partials.map(async partial => await load(`/.templates/${set.templates.selected}/partials/${escape(partial)}.ejs`))) await Promise.all(partials.map(async partial => await load(`/.templates/${set.templates.selected}/partials/${escape(partial)}.ejs`)))
//Trap includes //Trap includes
image = image.replace(/<%-\s*await include[(](`.*?[.]ejs`)[)]\s*%>/g, (m, g) => `<%- await $include(${g}) %>`) image = image.replace(/<%-\s*await include[(](`.*?[.]ejs`)[)]\s*%>/g, (m, g) => `<%- await $include(${g}) %>`)
@@ -45,11 +45,11 @@
partials: new Set([...(set.config.order || "").split(",").map(x => x.trim()).filter(x => partials.includes(x)), ...partials]), partials: new Set([...(set.config.order || "").split(",").map(x => x.trim()).filter(x => partials.includes(x)), ...partials]),
//Plural helper //Plural helper
s(value, end = "") { s(value, end = "") {
return value !== 1 ? { y: "ies", "": "s" }[end] : end return value !== 1 ? {y: "ies", "": "s"}[end] : end
}, },
//Formatter helper //Formatter helper
f(n, { sign = false } = {}) { f(n, {sign = false} = {}) {
for (const { u, v } of [{ u: "b", v: 10 ** 9 }, { u: "m", v: 10 ** 6 }, { u: "k", v: 10 ** 3 }]) { for (const {u, v} of [{u: "b", v: 10 ** 9}, {u: "m", v: 10 ** 6}, {u: "k", v: 10 ** 3}]) {
if (n / v >= 1) if (n / v >= 1)
return `${(sign) && (n > 0) ? "+" : ""}${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}` return `${(sign) && (n > 0) ? "+" : ""}${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
} }
@@ -58,10 +58,10 @@
//Trap for includes //Trap for includes
async $include(path) { async $include(path) {
const partial = await load(`/.templates/${set.templates.selected}/${escape(path)}`) const partial = await load(`/.templates/${set.templates.selected}/${escape(path)}`)
return await ejs.render(partial, data, { async: true, rmWhitespace: true }) return await ejs.render(partial, data, {async: true, rmWhitespace: true})
}, },
//Meta-data //Meta-data
meta: { version: set.version, author: "lowlighter", generated: new Date().toGMTString().replace(/GMT$/g, "").trim() }, meta: {version: set.version, author: "lowlighter", generated: new Date().toGMTString().replace(/GMT$/g, "").trim()},
//Animated //Animated
animated: false, animated: false,
//Display size //Display size
@@ -70,30 +70,30 @@
//Config //Config
config: set.config, config: set.config,
//Extras //Extras
extras: { css: options["extras.css"] ?? "" }, extras: {css: options["extras.css"] ?? ""},
//Base elements //Base elements
base: set.plugins.enabled.base, base: set.plugins.enabled.base,
//Computed elements //Computed elements
computed: { computed: {
commits: faker.datatype.number(10000), commits: faker.datatype.number(10000),
sponsorships: faker.datatype.number(10), sponsorships: faker.datatype.number(10),
licenses: { favorite: [""], used: { MIT: 1 }, about: {} }, licenses: {favorite: [""], used: {MIT: 1}, about: {}},
token: { scopes: [] }, token: {scopes: []},
repositories: { repositories: {
watchers: faker.datatype.number(1000), watchers: faker.datatype.number(1000),
stargazers: faker.datatype.number(10000), stargazers: faker.datatype.number(10000),
issues_open: faker.datatype.number(1000), issues_open: faker.datatype.number(1000),
issues_closed: faker.datatype.number(1000), issues_closed: faker.datatype.number(1000),
pr_open: faker.datatype.number(1000), pr_open: faker.datatype.number(1000),
pr_closed: { totalCount: faker.datatype.number(100) }, pr_closed: {totalCount: faker.datatype.number(100)},
pr_merged: faker.datatype.number(1000), pr_merged: faker.datatype.number(1000),
forks: faker.datatype.number(1000), forks: faker.datatype.number(1000),
releases: faker.datatype.number(1000), releases: faker.datatype.number(1000),
}, },
diskUsage: `${faker.datatype.float({ min: 1, max: 999 }).toFixed(1)}MB`, diskUsage: `${faker.datatype.float({min: 1, max: 999}).toFixed(1)}MB`,
registration: `${faker.datatype.number({ min: 2, max: 10 })} years ago`, registration: `${faker.datatype.number({min: 2, max: 10})} years ago`,
cakeday: false, cakeday: false,
calendar: new Array(14).fill(null).map(_ => ({ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) })), calendar: new Array(14).fill(null).map(_ => ({color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])})),
avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
}, },
//User data //User data
@@ -107,12 +107,12 @@
websiteUrl: options["pagespeed.url"] || "(attached website)", websiteUrl: options["pagespeed.url"] || "(attached website)",
isHireable: false, isHireable: false,
twitterUsername: options["tweets.user"] || "(attached Twitter account)", twitterUsername: options["tweets.user"] || "(attached Twitter account)",
repositories: { totalCount: faker.datatype.number(100), totalDiskUsage: faker.datatype.number(100000), nodes: [] }, repositories: {totalCount: faker.datatype.number(100), totalDiskUsage: faker.datatype.number(100000), nodes: []},
packages: { totalCount: faker.datatype.number(10) }, packages: {totalCount: faker.datatype.number(10)},
starredRepositories: { totalCount: faker.datatype.number(1000) }, starredRepositories: {totalCount: faker.datatype.number(1000)},
watching: { totalCount: faker.datatype.number(100) }, watching: {totalCount: faker.datatype.number(100)},
sponsorshipsAsSponsor: { totalCount: faker.datatype.number(10) }, sponsorshipsAsSponsor: {totalCount: faker.datatype.number(10)},
sponsorshipsAsMaintainer: { totalCount: faker.datatype.number(10) }, sponsorshipsAsMaintainer: {totalCount: faker.datatype.number(10)},
contributionsCollection: { contributionsCollection: {
totalRepositoriesWithContributedCommits: faker.datatype.number(100), totalRepositoriesWithContributedCommits: faker.datatype.number(100),
totalCommitContributions: faker.datatype.number(10000), totalCommitContributions: faker.datatype.number(10000),
@@ -121,12 +121,12 @@
totalPullRequestContributions: faker.datatype.number(1000), totalPullRequestContributions: faker.datatype.number(1000),
totalPullRequestReviewContributions: faker.datatype.number(1000), totalPullRequestReviewContributions: faker.datatype.number(1000),
}, },
calendar: { contributionCalendar: { weeks: [] } }, calendar: {contributionCalendar: {weeks: []}},
repositoriesContributedTo: { totalCount: faker.datatype.number(100) }, repositoriesContributedTo: {totalCount: faker.datatype.number(100)},
followers: { totalCount: faker.datatype.number(1000) }, followers: {totalCount: faker.datatype.number(1000)},
following: { totalCount: faker.datatype.number(1000) }, following: {totalCount: faker.datatype.number(1000)},
issueComments: { totalCount: faker.datatype.number(1000) }, issueComments: {totalCount: faker.datatype.number(1000)},
organizations: { totalCount: faker.datatype.number(10) }, organizations: {totalCount: faker.datatype.number(10)},
}, },
//Plugins //Plugins
plugins: { plugins: {
@@ -148,7 +148,7 @@
id: faker.datatype.number(100000000000000).toString(), id: faker.datatype.number(100000000000000).toString(),
created_at: faker.date.recent(), created_at: faker.date.recent(),
entities: { entities: {
mentions: [{ start: 22, end: 33, username: "lowlighter" }], mentions: [{start: 22, end: 33, username: "lowlighter"}],
}, },
text: 'Checkout metrics from <span class="mention">@lowlighter</span> ! <span class="hashtag">#GitHub</span> ', text: 'Checkout metrics from <span class="mention">@lowlighter</span> ! <span class="hashtag">#GitHub</span> ',
mentions: ["lowlighter"], mentions: ["lowlighter"],
@@ -177,7 +177,7 @@
? ({ ? ({
traffic: { traffic: {
views: { views: {
count: `${faker.datatype.number({ min: 10, max: 100 })}.${faker.datatype.number(9)}k`, count: `${faker.datatype.number({min: 10, max: 100})}.${faker.datatype.number(9)}k`,
uniques: `${faker.datatype.number(10)}.${faker.datatype.number(9)}k`, uniques: `${faker.datatype.number(10)}.${faker.datatype.number(9)}k`,
}, },
}, },
@@ -264,7 +264,7 @@
? { ? {
user: { user: {
commits: faker.datatype.number(100), commits: faker.datatype.number(100),
percentage: faker.datatype.float({ max: 1 }), percentage: faker.datatype.float({max: 1}),
maintainer: false, maintainer: false,
stars: faker.datatype.number(100), stars: faker.datatype.number(100),
}, },
@@ -365,7 +365,7 @@
unlock: null, unlock: null,
text: faker.lorem.sentence(), text: faker.lorem.sentence(),
get icon() { get icon() {
const colors = { S: ["#FF0000", "#FF8500"], A: ["#B59151", "#FFD576"], B: ["#7D6CFF", "#B2A8FF"], C: ["#2088FF", "#79B8FF"], $: ["#FF48BD", "#FF92D8"], X: ["#7A7A7A", "#B0B0B0"] } const colors = {S: ["#FF0000", "#FF8500"], A: ["#B59151", "#FFD576"], B: ["#7D6CFF", "#B2A8FF"], C: ["#2088FF", "#79B8FF"], $: ["#FF48BD", "#FF92D8"], X: ["#7A7A7A", "#B0B0B0"]}
return `<g xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke-linejoin="round"><g stroke="#secondary"><path d="M8 43a3 3 0 100 6 3 3 0 000-6zm40 0a3.001 3.001 0 10.002 6.002A3.001 3.001 0 0048 43zm-18 3h-4.971m-11.045 0H11M45 46h-4"/></g><path stroke="#primary" d="M13 51h28M36.992 45.276l6.375-8.017c1.488.63 3.272.29 4.414-.977a3.883 3.883 0 00.658-4.193l-1.96 2.174-1.936-.151-.406-1.955 1.96-2.173a3.898 3.898 0 00-4.107 1.092 3.886 3.886 0 00-.512 4.485l-7.317 7.169c-1.32 1.314-.807 2.59-.236 3.105.67.601 1.888.845 3.067-.56z"/><g stroke="#primary"><path d="M12.652 31.063l9.442 12.578a.512.512 0 01-.087.716l-2.396 1.805a.512.512 0 01-.712-.114L9.46 33.47l-.176-3.557 3.37 1.15zM17.099 43.115l2.395-1.806"/></g></g><path d="M25.68 36.927v-2.54a2.227 2.227 0 01.37-1.265c-.526-.04-3.84-.371-3.84-4.302 0-1.013.305-1.839.915-2.477a4.989 4.989 0 01-.146-1.86c.087-.882.946-.823 2.577.178 1.277-.47 2.852-.47 4.725 0 .248-.303 2.434-1.704 2.658-.268.047.296.016.946-.093 1.95.516.524.776 1.358.78 2.501.007 2.261-1.26 3.687-3.8 4.278.24.436.355.857.346 1.264a117.57 117.57 0 000 2.614c2.43-.744 4.228-2.06 5.395-3.95.837-1.356 1.433-2.932 1.433-4.865 0-2.886-1.175-4.984-2.5-6.388C32.714 19.903 30.266 19 28 19a9.094 9.094 0 00-6.588 2.897C20.028 23.393 19 25.507 19 28.185c0 2.026.701 3.945 1.773 5.38 1.228 1.643 2.864 2.764 4.907 3.362zM52.98 25.002l-3.07 3.065-1.49-1.485M6.98 25.002l-3.07 3.065-1.49-1.485" stroke="#primary" stroke-linejoin="round"/><path d="M19.001 11V9a2 2 0 012-2h14a2 2 0 012 2v2m-21 12.028v-10.03a2 2 0 012-1.998h20a2 2 0 012 2v10.028" stroke="#secondary" stroke-linejoin="round"/><path stroke="#secondary" d="M28.001 7V3M15.039 7.797c-5.297 3.406-9.168 8.837-10.517 15.2m46.737-.936c-1.514-5.949-5.25-11.01-10.273-14.248"/></g>` return `<g xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke-linejoin="round"><g stroke="#secondary"><path d="M8 43a3 3 0 100 6 3 3 0 000-6zm40 0a3.001 3.001 0 10.002 6.002A3.001 3.001 0 0048 43zm-18 3h-4.971m-11.045 0H11M45 46h-4"/></g><path stroke="#primary" d="M13 51h28M36.992 45.276l6.375-8.017c1.488.63 3.272.29 4.414-.977a3.883 3.883 0 00.658-4.193l-1.96 2.174-1.936-.151-.406-1.955 1.96-2.173a3.898 3.898 0 00-4.107 1.092 3.886 3.886 0 00-.512 4.485l-7.317 7.169c-1.32 1.314-.807 2.59-.236 3.105.67.601 1.888.845 3.067-.56z"/><g stroke="#primary"><path d="M12.652 31.063l9.442 12.578a.512.512 0 01-.087.716l-2.396 1.805a.512.512 0 01-.712-.114L9.46 33.47l-.176-3.557 3.37 1.15zM17.099 43.115l2.395-1.806"/></g></g><path d="M25.68 36.927v-2.54a2.227 2.227 0 01.37-1.265c-.526-.04-3.84-.371-3.84-4.302 0-1.013.305-1.839.915-2.477a4.989 4.989 0 01-.146-1.86c.087-.882.946-.823 2.577.178 1.277-.47 2.852-.47 4.725 0 .248-.303 2.434-1.704 2.658-.268.047.296.016.946-.093 1.95.516.524.776 1.358.78 2.501.007 2.261-1.26 3.687-3.8 4.278.24.436.355.857.346 1.264a117.57 117.57 0 000 2.614c2.43-.744 4.228-2.06 5.395-3.95.837-1.356 1.433-2.932 1.433-4.865 0-2.886-1.175-4.984-2.5-6.388C32.714 19.903 30.266 19 28 19a9.094 9.094 0 00-6.588 2.897C20.028 23.393 19 25.507 19 28.185c0 2.026.701 3.945 1.773 5.38 1.228 1.643 2.864 2.764 4.907 3.362zM52.98 25.002l-3.07 3.065-1.49-1.485M6.98 25.002l-3.07 3.065-1.49-1.485" stroke="#primary" stroke-linejoin="round"/><path d="M19.001 11V9a2 2 0 012-2h14a2 2 0 012 2v2m-21 12.028v-10.03a2 2 0 012-1.998h20a2 2 0 012 2v10.028" stroke="#secondary" stroke-linejoin="round"/><path stroke="#secondary" d="M28.001 7V3M15.039 7.797c-5.297 3.406-9.168 8.837-10.517 15.2m46.737-.936c-1.514-5.949-5.25-11.01-10.273-14.248"/></g>`
.replace(/#primary/g, colors[this.rank][0]) .replace(/#primary/g, colors[this.rank][0])
.replace(/#secondary/g, colors[this.rank][1]) .replace(/#secondary/g, colors[this.rank][1])
@@ -374,9 +374,9 @@
progress: faker.datatype.number(100) / 100, progress: faker.datatype.number(100) / 100,
value: faker.datatype.number(1000), value: faker.datatype.number(1000),
})) }))
.filter(({ rank }) => options["achievements.secrets"] ? true : rank !== "$") .filter(({rank}) => options["achievements.secrets"] ? true : rank !== "$")
.filter(({ rank }) => ({ S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[rank] >= { S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[options["achievements.threshold"]])) .filter(({rank}) => ({S: 5, A: 4, B: 3, C: 2, $: 1, X: 0}[rank] >= {S: 5, A: 4, B: 3, C: 2, $: 1, X: 0}[options["achievements.threshold"]]))
.sort((a, b) => ({ S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[b.rank] + b.progress * 0.99) - ({ S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[a.rank] + a.progress * 0.99)) .sort((a, b) => ({S: 5, A: 4, B: 3, C: 2, $: 1, X: 0}[b.rank] + b.progress * 0.99) - ({S: 5, A: 4, B: 3, C: 2, $: 1, X: 0}[a.rank] + a.progress * 0.99))
.slice(0, options["achievements.limit"] || Infinity), .slice(0, options["achievements.limit"] || Infinity),
}, },
}) })
@@ -437,26 +437,26 @@
sections: options["languages.sections"].split(", ").map(x => x.trim()).filter(x => /^(most-used|recently-used)$/.test(x)), sections: options["languages.sections"].split(", ").map(x => x.trim()).filter(x => /^(most-used|recently-used)$/.test(x)),
details: options["languages.details"].split(",").map(x => x.trim()).filter(x => x), details: options["languages.details"].split(",").map(x => x.trim()).filter(x => x),
get colors() { get colors() {
return Object.fromEntries(Object.entries(this.favorites).map(([key, { color }]) => [key, color])) return Object.fromEntries(Object.entries(this.favorites).map(([key, {color}]) => [key, color]))
}, },
total: faker.datatype.number(10000), total: faker.datatype.number(10000),
get stats() { get stats() {
return Object.fromEntries(Object.entries(this.favorites).map(([key, { value }]) => [key, value])) return Object.fromEntries(Object.entries(this.favorites).map(([key, {value}]) => [key, value]))
}, },
["stats.recent"]: { ["stats.recent"]: {
total: faker.datatype.number(10000), total: faker.datatype.number(10000),
get lines() { get lines() {
return Object.fromEntries(Object.entries(this.favorites).map(([key, { value }]) => [key, value])) return Object.fromEntries(Object.entries(this.favorites).map(([key, {value}]) => [key, value]))
}, },
get stats() { get stats() {
return Object.fromEntries(Object.entries(this.favorites).map(([key, { value }]) => [key, value])) return Object.fromEntries(Object.entries(this.favorites).map(([key, {value}]) => [key, value]))
}, },
commits: faker.datatype.number(500), commits: faker.datatype.number(500),
files: faker.datatype.number(1000), files: faker.datatype.number(1000),
days: Number(options["languages.recent.days"]), days: Number(options["languages.recent.days"]),
}, },
favorites: distribution(7).map((value, index, array) => ({ name: faker.lorem.word(), color: faker.internet.color(), value, size: faker.datatype.number(1000000), x: array.slice(0, index).reduce((a, b) => a + b, 0) })), favorites: distribution(7).map((value, index, array) => ({name: faker.lorem.word(), color: faker.internet.color(), value, size: faker.datatype.number(1000000), x: array.slice(0, index).reduce((a, b) => a + b, 0)})),
recent: distribution(7).map((value, index, array) => ({ name: faker.lorem.word(), color: faker.internet.color(), value, size: faker.datatype.number(1000000), x: array.slice(0, index).reduce((a, b) => a + b, 0) })), recent: distribution(7).map((value, index, array) => ({name: faker.lorem.word(), color: faker.internet.color(), value, size: faker.datatype.number(1000000), x: array.slice(0, index).reduce((a, b) => a + b, 0)})),
}, },
}) })
: null), : null),
@@ -536,7 +536,7 @@
}, },
}, },
}, },
indents: { style: "spaces", spaces: 1, tabs: 0 }, indents: {style: "spaces", spaces: 1, tabs: 0},
linguist: { linguist: {
available: true, available: true,
get ordered() { get ordered() {
@@ -554,7 +554,7 @@
? ({ ? ({
get people() { get people() {
const types = options["people.types"].split(",").map(x => x.trim()) const types = options["people.types"].split(",").map(x => x.trim())
.map(x => ({ followed: "following", sponsors: "sponsorshipsAsMaintainer", sponsored: "sponsorshipsAsSponsor", sponsoring: "sponsorshipsAsSponsor" })[x] ?? x) .map(x => ({followed: "following", sponsors: "sponsorshipsAsMaintainer", sponsored: "sponsorshipsAsSponsor", sponsoring: "sponsorshipsAsSponsor"})[x] ?? x)
.filter(x => ["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor"].includes(x)) .filter(x => ["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor"].includes(x))
return { return {
types, types,
@@ -598,8 +598,8 @@
data: new Array(12).fill(null).map(_ => ({ data: new Array(12).fill(null).map(_ => ({
timeUTCHumanReadable: `${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`, timeUTCHumanReadable: `${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`,
color: faker.random.arrayElement(["#9be9a8", "#40c463", "#30a14e", "#216e39"]), color: faker.random.arrayElement(["#9be9a8", "#40c463", "#30a14e", "#216e39"]),
sgv: faker.datatype.number({ min: 40, max: 400 }), sgv: faker.datatype.number({min: 40, max: 400}),
delta: faker.datatype.number({ min: -10, max: 10 }), delta: faker.datatype.number({min: -10, max: 10}),
direction: faker.random.arrayElement(["SingleUp", "DoubleUp", "FortyFiveUp", "Flat", "FortyFiveDown", "SingleDown", "DoubleDown"]), direction: faker.random.arrayElement(["SingleUp", "DoubleUp", "FortyFiveUp", "Flat", "FortyFiveDown", "SingleDown", "DoubleDown"]),
alert: faker.random.arrayElement(["Normal", "Urgent High", "Urgent Low", "High", "Low"]), alert: faker.random.arrayElement(["Normal", "Urgent High", "Urgent Low", "High", "Low"]),
arrowHumanReadable: faker.random.arrayElement(["↑↑", "↑", "↗", "→", "↘", "↓", "↓↓"]), arrowHumanReadable: faker.random.arrayElement(["↑↑", "↑", "↗", "→", "↘", "↓", "↓↓"]),
@@ -611,9 +611,9 @@
...(set.plugins.enabled.fortune ...(set.plugins.enabled.fortune
? ({ ? ({
fortune: faker.random.arrayElement([ fortune: faker.random.arrayElement([
{ chance: .06, color: "#43FD3B", text: "Good news will come to you by mail" }, {chance: .06, color: "#43FD3B", text: "Good news will come to you by mail"},
{ chance: .06, color: "#00CBB0", text: "キタ━━━━━━(゚∀゚)━━━━━━ !!!!" }, {chance: .06, color: "#00CBB0", text: "キタ━━━━━━(゚∀゚)━━━━━━ !!!!"},
{ chance: 0.03, color: "#FD4D32", text: "Excellent Luck" }, {chance: 0.03, color: "#FD4D32", text: "Excellent Luck"},
]), ]),
}) })
: null), : null),
@@ -624,10 +624,10 @@
url: options["pagespeed.url"] || "(attached website url)", url: options["pagespeed.url"] || "(attached website url)",
detailed: options["pagespeed.detailed"] || false, detailed: options["pagespeed.detailed"] || false,
scores: [ scores: [
{ score: faker.datatype.float({ max: 1 }), title: "Performance" }, {score: faker.datatype.float({max: 1}), title: "Performance"},
{ score: faker.datatype.float({ max: 1 }), title: "Accessibility" }, {score: faker.datatype.float({max: 1}), title: "Accessibility"},
{ score: faker.datatype.float({ max: 1 }), title: "Best Practices" }, {score: faker.datatype.float({max: 1}), title: "Best Practices"},
{ score: faker.datatype.float({ max: 1 }), title: "SEO" }, {score: faker.datatype.float({max: 1}), title: "SEO"},
], ],
metrics: { metrics: {
observedFirstContentfulPaint: faker.datatype.number(500), observedFirstContentfulPaint: faker.datatype.number(500),
@@ -639,12 +639,12 @@
maxPotentialFID: faker.datatype.number(500), maxPotentialFID: faker.datatype.number(500),
observedLoad: faker.datatype.number(500), observedLoad: faker.datatype.number(500),
firstMeaningfulPaint: faker.datatype.number(500), firstMeaningfulPaint: faker.datatype.number(500),
observedCumulativeLayoutShift: faker.datatype.float({ max: 1 }), observedCumulativeLayoutShift: faker.datatype.float({max: 1}),
observedSpeedIndex: faker.datatype.number(1000), observedSpeedIndex: faker.datatype.number(1000),
observedSpeedIndexTs: faker.time.recent(), observedSpeedIndexTs: faker.time.recent(),
observedTimeOriginTs: faker.time.recent(), observedTimeOriginTs: faker.time.recent(),
observedLargestContentfulPaint: faker.datatype.number(1000), observedLargestContentfulPaint: faker.datatype.number(1000),
cumulativeLayoutShift: faker.datatype.float({ max: 1 }), cumulativeLayoutShift: faker.datatype.float({max: 1}),
observedFirstPaintTs: faker.time.recent(), observedFirstPaintTs: faker.time.recent(),
observedTraceEndTs: faker.time.recent(), observedTraceEndTs: faker.time.recent(),
largestContentfulPaint: faker.datatype.number(2000), largestContentfulPaint: faker.datatype.number(2000),
@@ -698,14 +698,14 @@
? ({ ? ({
discussions: { discussions: {
categories: { categories: {
stats: { "🙏 Q&A": faker.datatype.number(100), "📣 Announcements": faker.datatype.number(100), "💡 Ideas": faker.datatype.number(100), "💬 General": faker.datatype.number(100) }, stats: {"🙏 Q&A": faker.datatype.number(100), "📣 Announcements": faker.datatype.number(100), "💡 Ideas": faker.datatype.number(100), "💬 General": faker.datatype.number(100)},
favorite: "📣 Announcements", favorite: "📣 Announcements",
}, },
upvotes: { discussions: faker.datatype.number(1000), comments: faker.datatype.number(1000) }, upvotes: {discussions: faker.datatype.number(1000), comments: faker.datatype.number(1000)},
started: faker.datatype.number(1000), started: faker.datatype.number(1000),
comments: faker.datatype.number(1000), comments: faker.datatype.number(1000),
answers: faker.datatype.number(1000), answers: faker.datatype.number(1000),
display: { categories: options["discussions.categories"] ? { limit: options["discussions.categories.limit"] || Infinity } : null }, display: {categories: options["discussions.categories"] ? {limit: options["discussions.categories.limit"] || Infinity} : null},
}, },
}) })
: null), : null),
@@ -730,7 +730,7 @@
? ({ ? ({
topics: { topics: {
mode: options["topics.mode"], mode: options["topics.mode"],
type: { starred: "labels", labels: "labels", mastered: "icons", icons: "icons" }[options["topics.mode"]] || "labels", type: {starred: "labels", labels: "labels", mastered: "icons", icons: "icons"}[options["topics.mode"]] || "labels",
list: new Array(Number(options["topics.limit"]) || 20).fill(null).map(_ => ({ list: new Array(Number(options["topics.limit"]) || 20).fill(null).map(_ => ({
name: faker.lorem.words(2), name: faker.lorem.words(2),
description: faker.lorem.sentence(), description: faker.lorem.sentence(),
@@ -759,8 +759,8 @@
totalCount: faker.datatype.number(100), totalCount: faker.datatype.number(100),
}, },
stargazerCount: faker.datatype.number(10000), stargazerCount: faker.datatype.number(10000),
licenseInfo: { nickname: null, name: "MIT License" }, licenseInfo: {nickname: null, name: "MIT License"},
primaryLanguage: { color: "#f1e05a", name: "JavaScript" }, primaryLanguage: {color: "#f1e05a", name: "JavaScript"},
}, },
starred: "1 day ago", starred: "1 day ago",
}, },
@@ -779,8 +779,8 @@
totalCount: faker.datatype.number(100), totalCount: faker.datatype.number(100),
}, },
stargazerCount: faker.datatype.number(10000), stargazerCount: faker.datatype.number(10000),
licenseInfo: { nickname: null, name: "License" }, licenseInfo: {nickname: null, name: "License"},
primaryLanguage: { color: faker.internet.color(), name: faker.lorem.word() }, primaryLanguage: {color: faker.internet.color(), name: faker.lorem.word()},
}, },
starred: `${i + 2} days ago`, starred: `${i + 2} days ago`,
})), })),
@@ -825,8 +825,8 @@
totalCount: faker.datatype.number(100), totalCount: faker.datatype.number(100),
}, },
stargazerCount: faker.datatype.number(10000), stargazerCount: faker.datatype.number(10000),
licenseInfo: { nickname: null, name: "License" }, licenseInfo: {nickname: null, name: "License"},
primaryLanguage: { color: faker.internet.color(), name: faker.lorem.word() }, primaryLanguage: {color: faker.internet.color(), name: faker.lorem.word()},
})), })),
}, },
}) })
@@ -861,7 +861,7 @@
for (let d = -14; d <= 0; d++) { for (let d = -14; d <= 0; d++) {
const date = new Date(Date.now() - d * 24 * 60 * 60 * 1000).toISOString().substring(0, 10) const date = new Date(Date.now() - d * 24 * 60 * 60 * 1000).toISOString().substring(0, 10)
dates.push(date) dates.push(date)
result.total.dates[date] = (total += (result.increments.dates[date] = faker.datatype.number(100))) result.total.dates[date] = total += result.increments.dates[date] = faker.datatype.number(100)
} }
return result return result
}, },
@@ -884,13 +884,13 @@
percents -= result.percent percents -= result.percent
result.percent /= 100 result.percent /= 100
} }
results.filter(({ name }) => elements.includes(name) ? false : (elements.push(name), true)) results.filter(({name}) => elements.includes(name) ? false : (elements.push(name), true))
return results.sort((a, b) => b.percent - a.percent) return results.sort((a, b) => b.percent - a.percent)
} }
return { return {
sections: options["wakatime.sections"].split(",").map(x => x.trim()).filter(x => x), sections: options["wakatime.sections"].split(",").map(x => x.trim()).filter(x => x),
days: Number(options["wakatime.days"]) || 7, days: Number(options["wakatime.days"]) || 7,
time: { total: faker.datatype.number(100000), daily: faker.datatype.number(24) }, time: {total: faker.datatype.number(100000), daily: faker.datatype.number(24)},
editors: stats(["VS Code", "Chrome", "IntelliJ", "PhpStorm", "WebStorm", "Android Studio", "Visual Studio", "Sublime Text", "PyCharm", "Vim", "Atom", "Xcode"]), editors: stats(["VS Code", "Chrome", "IntelliJ", "PhpStorm", "WebStorm", "Android Studio", "Visual Studio", "Sublime Text", "PyCharm", "Vim", "Atom", "Xcode"]),
languages: stats(["JavaScript", "TypeScript", "PHP", "Java", "Python", "Vue.js", "HTML", "C#", "JSON", "Dart", "SCSS", "Kotlin", "JSX", "Go", "Ruby", "YAML"]), languages: stats(["JavaScript", "TypeScript", "PHP", "Java", "Python", "Vue.js", "HTML", "C#", "JSON", "Dart", "SCSS", "Kotlin", "JSX", "Go", "Ruby", "YAML"]),
projects: stats(), projects: stats(),
@@ -909,16 +909,16 @@
count: faker.datatype.number(1000), count: faker.datatype.number(1000),
minutesWatched: faker.datatype.number(100000), minutesWatched: faker.datatype.number(100000),
episodesWatched: faker.datatype.number(10000), episodesWatched: faker.datatype.number(10000),
genres: new Array(4).fill(null).map(_ => ({ genre: faker.lorem.word() })), genres: new Array(4).fill(null).map(_ => ({genre: faker.lorem.word()})),
}, },
manga: { manga: {
count: faker.datatype.number(1000), count: faker.datatype.number(1000),
chaptersRead: faker.datatype.number(100000), chaptersRead: faker.datatype.number(100000),
volumesRead: faker.datatype.number(10000), volumesRead: faker.datatype.number(10000),
genres: new Array(4).fill(null).map(_ => ({ genre: faker.lorem.word() })), genres: new Array(4).fill(null).map(_ => ({genre: faker.lorem.word()})),
}, },
}, },
genres: new Array(4).fill(null).map(_ => ({ genre: faker.lorem.word() })), genres: new Array(4).fill(null).map(_ => ({genre: faker.lorem.word()})),
}, },
get lists() { get lists() {
const media = type => ({ const media = type => ({
@@ -929,7 +929,7 @@
genres: new Array(6).fill(null).map(_ => faker.lorem.word()), genres: new Array(6).fill(null).map(_ => faker.lorem.word()),
progress: faker.datatype.number(100), progress: faker.datatype.number(100),
description: faker.lorem.paragraphs(), description: faker.lorem.paragraphs(),
scores: { user: faker.datatype.number(100), community: faker.datatype.number(100) }, scores: {user: faker.datatype.number(100), community: faker.datatype.number(100)},
released: 100 + faker.datatype.number(1000), released: 100 + faker.datatype.number(1000),
artwork: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", artwork: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
}) })
@@ -939,16 +939,16 @@
...(medias.includes("anime") ...(medias.includes("anime")
? { ? {
anime: { anime: {
...(sections.includes("watching") ? { watching: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("ANIME")) } : {}), ...(sections.includes("watching") ? {watching: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("ANIME"))} : {}),
...(sections.includes("favorites") ? { favorites: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("ANIME")) } : {}), ...(sections.includes("favorites") ? {favorites: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("ANIME"))} : {}),
}, },
} }
: {}), : {}),
...(medias.includes("manga") ...(medias.includes("manga")
? { ? {
manga: { manga: {
...(sections.includes("reading") ? { reading: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("MANGA")) } : {}), ...(sections.includes("reading") ? {reading: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("MANGA"))} : {}),
...(sections.includes("favorites") ? { favorites: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("MANGA")) } : {}), ...(sections.includes("favorites") ? {favorites: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("MANGA"))} : {}),
}, },
} }
: {}), : {}),
@@ -974,7 +974,7 @@
repo: `${faker.random.word()}/${faker.random.word()}`, repo: `${faker.random.word()}/${faker.random.word()}`,
size: 1, size: 1,
branch: "master", branch: "master",
commits: [{ sha: faker.git.shortSha(), message: faker.lorem.sentence() }], commits: [{sha: faker.git.shortSha(), message: faker.lorem.sentence()}],
timestamp: faker.date.recent(), timestamp: faker.date.recent(),
}, },
{ {
@@ -1026,8 +1026,8 @@
user: set.user, user: set.user,
number: faker.datatype.number(100), number: faker.datatype.number(100),
title: faker.lorem.sentence(), title: faker.lorem.sentence(),
lines: { added: faker.datatype.number(1000), deleted: faker.datatype.number(1000) }, lines: {added: faker.datatype.number(1000), deleted: faker.datatype.number(1000)},
files: { changed: faker.datatype.number(10) }, files: {changed: faker.datatype.number(10)},
timestamp: faker.date.recent(), timestamp: faker.date.recent(),
}, },
{ {
@@ -1061,13 +1061,13 @@
{ {
type: "ref/create", type: "ref/create",
repo: `${faker.random.word()}/${faker.random.word()}`, repo: `${faker.random.word()}/${faker.random.word()}`,
ref: { name: faker.lorem.slug(), type: faker.random.arrayElement(["tag", "branch"]) }, ref: {name: faker.lorem.slug(), type: faker.random.arrayElement(["tag", "branch"])},
timestamp: faker.date.recent(), timestamp: faker.date.recent(),
}, },
{ {
type: "ref/delete", type: "ref/delete",
repo: `${faker.random.word()}/${faker.random.word()}`, repo: `${faker.random.word()}/${faker.random.word()}`,
ref: { name: faker.lorem.slug(), type: faker.random.arrayElement(["tag", "branch"]) }, ref: {name: faker.lorem.slug(), type: faker.random.arrayElement(["tag", "branch"])},
timestamp: faker.date.recent(), timestamp: faker.date.recent(),
}, },
{ {
@@ -1096,7 +1096,7 @@
...(set.plugins.enabled.isocalendar ...(set.plugins.enabled.isocalendar
? ({ ? ({
isocalendar: { isocalendar: {
streak: { max: 30 + faker.datatype.number(20), current: faker.datatype.number(30) }, streak: {max: 30 + faker.datatype.number(20), current: faker.datatype.number(30)},
max: 10 + faker.datatype.number(40), max: 10 + faker.datatype.number(40),
average: faker.datatype.float(10), average: faker.datatype.float(10),
svg: await staticPlaceholder(set.plugins.enabled.isocalendar, `isocalendar.${options["isocalendar.duration"]}.svg`), svg: await staticPlaceholder(set.plugins.enabled.isocalendar, `isocalendar.${options["isocalendar.duration"]}.svg`),
@@ -1108,8 +1108,8 @@
...(set.plugins.enabled.support ...(set.plugins.enabled.support
? ({ ? ({
support: { support: {
stats: { solutions: faker.datatype.number(100), posts: faker.datatype.number(1000), topics: faker.datatype.number(1000), received: faker.datatype.number(1000), hearts: faker.datatype.number(1000) }, stats: {solutions: faker.datatype.number(100), posts: faker.datatype.number(1000), topics: faker.datatype.number(1000), received: faker.datatype.number(1000), hearts: faker.datatype.number(1000)},
badges: { uniques: [], multiples: [], count: faker.datatype.number(1000) }, badges: {uniques: [], multiples: [], count: faker.datatype.number(1000)},
}, },
}) })
: null), : null),
@@ -1200,20 +1200,20 @@
} }
//Formatters //Formatters
data.f.bytes = function(n) { data.f.bytes = function(n) {
for (const { u, v } of [{ u: "E", v: 10 ** 18 }, { u: "P", v: 10 ** 15 }, { u: "T", v: 10 ** 12 }, { u: "G", v: 10 ** 9 }, { u: "M", v: 10 ** 6 }, { u: "k", v: 10 ** 3 }]) { for (const {u, v} of [{u: "E", v: 10 ** 18}, {u: "P", v: 10 ** 15}, {u: "T", v: 10 ** 12}, {u: "G", v: 10 ** 9}, {u: "M", v: 10 ** 6}, {u: "k", v: 10 ** 3}]) {
if (n / v >= 1) if (n / v >= 1)
return `${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B` return `${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B`
} }
return `${n} byte${n > 1 ? "s" : ""}` return `${n} byte${n > 1 ? "s" : ""}`
} }
data.f.percentage = function(n, { rescale = true } = {}) { data.f.percentage = function(n, {rescale = true} = {}) {
return `${ return `${
(n * (rescale ? 100 : 1)).toFixed(2) (n * (rescale ? 100 : 1)).toFixed(2)
.replace(/[.]([1-9]*)(0+)$/, (m, a, b) => `.${a}`) .replace(/[.]([1-9]*)(0+)$/, (m, a, b) => `.${a}`)
.replace(/[.]$/, "") .replace(/[.]$/, "")
}%` }%`
} }
data.f.ellipsis = function(text, { length = 20 } = {}) { data.f.ellipsis = function(text, {length = 20} = {}) {
text = `${text}` text = `${text}`
if (text.length < length) if (text.length < length)
return text return text
@@ -1222,11 +1222,11 @@
data.f.date = function(string, options) { data.f.date = function(string, options) {
if (options.date) { if (options.date) {
delete options.date delete options.date
Object.assign(options, { day: "numeric", month: "short", year: "numeric" }) Object.assign(options, {day: "numeric", month: "short", year: "numeric"})
} }
if (options.time) { if (options.time) {
delete options.time delete options.time
Object.assign(options, { hour: "2-digit", minute: "2-digit", second: "2-digit" }) Object.assign(options, {hour: "2-digit", minute: "2-digit", second: "2-digit"})
} }
return new Intl.DateTimeFormat("en-GB", options).format(new Date(string)) return new Intl.DateTimeFormat("en-GB", options).format(new Date(string))
} }
@@ -1234,7 +1234,7 @@
return text?.name ?? text return text?.name ?? text
} }
//Render //Render
return await ejs.render(image, data, { async: true, rmWhitespace: true }) return await ejs.render(image, data, {async: true, rmWhitespace: true})
} }
//Reset globals contexts //Reset globals contexts
globalThis.placeholder.init = function(globals) { globalThis.placeholder.init = function(globals) {

View File

@@ -18,48 +18,48 @@ export default async function({login, q, imports, data, computed, graphql, queri
await compute[account]({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) await compute[account]({list, login, data, computed, imports, graphql, queries, rank, leaderboard})
//Results //Results
const order = {S:5, A:4, B:3, C:2, $:1, X:0} const order = {S: 5, A: 4, B: 3, C: 2, $: 1, X: 0}
const colors = {S:["#EB355E", "#731237"], A:["#B59151", "#FFD576"], B:["#7D6CFF", "#B2A8FF"], C:["#2088FF", "#79B8FF"], $:["#FF48BD", "#FF92D8"], X:["#7A7A7A", "#B0B0B0"]} const colors = {S: ["#EB355E", "#731237"], A: ["#B59151", "#FFD576"], B: ["#7D6CFF", "#B2A8FF"], C: ["#2088FF", "#79B8FF"], $: ["#FF48BD", "#FF92D8"], X: ["#7A7A7A", "#B0B0B0"]}
const achievements = list const achievements = list
.filter(a => (order[a.rank] >= order[threshold]) || ((a.rank === "$") && (secrets))) .filter(a => (order[a.rank] >= order[threshold]) || ((a.rank === "$") && (secrets)))
.filter(a => (!only.length) || ((only.length) && (only.includes(a.title.toLocaleLowerCase())))) .filter(a => (!only.length) || ((only.length) && (only.includes(a.title.toLocaleLowerCase()))))
.filter(a => !ignored.includes(a.title.toLocaleLowerCase())) .filter(a => !ignored.includes(a.title.toLocaleLowerCase()))
.sort((a, b) => (order[b.rank] + b.progress * 0.99) - (order[a.rank] + a.progress * 0.99)) .sort((a, b) => (order[b.rank] + b.progress * 0.99) - (order[a.rank] + a.progress * 0.99))
.map(({title, unlock, ...achievement}) => ({ .map(({title, unlock, ...achievement}) => ({
prefix:({S:"Master", A:"Super", B:"Great"}[achievement.rank] ?? ""), prefix: ({S: "Master", A: "Super", B: "Great"}[achievement.rank] ?? ""),
title, title,
unlock:!/invalid date/i.test(unlock) ? `${imports.format.date(unlock, {time:true})} on ${imports.format.date(unlock, {date:true})}` : null, unlock: !/invalid date/i.test(unlock) ? `${imports.format.date(unlock, {time: true})} on ${imports.format.date(unlock, {date: true})}` : null,
...achievement, ...achievement,
})) }))
.map(({icon, ...achievement}) => ({icon:icon.replace(/#primary/g, colors[achievement.rank][0]).replace(/#secondary/g, colors[achievement.rank][1]), ...achievement})) .map(({icon, ...achievement}) => ({icon: icon.replace(/#primary/g, colors[achievement.rank][0]).replace(/#secondary/g, colors[achievement.rank][1]), ...achievement}))
.slice(0, limit || Infinity) .slice(0, limit || Infinity)
return {list:achievements, display} return {list: achievements, display}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }
/**Rank */ /**Rank */
function rank(x, [c, b, a, s, m]) { function rank(x, [c, b, a, s, m]) {
if (x >= s) if (x >= s)
return {rank:"S", progress:(x - s) / (m - s)} return {rank: "S", progress: (x - s) / (m - s)}
if (x >= a) if (x >= a)
return {rank:"A", progress:(x - a) / (m - a)} return {rank: "A", progress: (x - a) / (m - a)}
else if (x >= b) else if (x >= b)
return {rank:"B", progress:(x - b) / (a - b)} return {rank: "B", progress: (x - b) / (a - b)}
else if (x >= c) else if (x >= c)
return {rank:"C", progress:(x - c) / (b - c)} return {rank: "C", progress: (x - c) / (b - c)}
return {rank:"X", progress:x / c} return {rank: "X", progress: x / c}
} }
/**Leaderboards */ /**Leaderboards */
function leaderboard({user, type, requirement}) { function leaderboard({user, type, requirement}) {
return requirement return requirement
? { ? {
user:1 + user, user: 1 + user,
total:total[type], total: total[type],
type, type,
get top() { get top() {
return Number(`1${"0".repeat(Math.ceil(Math.log10(this.user)))}`) return Number(`1${"0".repeat(Math.ceil(Math.log10(this.user)))}`)

View File

@@ -1,3 +1,3 @@
//Exports //Exports
export {default as organization} from "./organizations.mjs" export { default as organization } from "./organizations.mjs"
export {default as user} from "./users.mjs" export { default as user } from "./users.mjs"

View File

@@ -2,22 +2,23 @@
export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) { export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) {
//Initialization //Initialization
const {organization} = await graphql(queries.achievements.organizations({login})) const {organization} = await graphql(queries.achievements.organizations({login}))
const scores = {followers:0, created:organization.repositories.totalCount, stars:organization.popular.nodes?.[0]?.stargazers?.totalCount ?? 0, forks:Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount))} const scores = {followers: 0, created: organization.repositories.totalCount, stars: organization.popular.nodes?.[0]?.stargazers?.totalCount ?? 0, forks: Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount))}
const ranks = await graphql(queries.achievements.ranking(scores)) const ranks = await graphql(queries.achievements.ranking(scores))
const requirements = {stars:5, followers:3, forks:1, created:1} const requirements = {stars: 5, followers: 3, forks: 1, created: 1}
//Developers //Developers
{ {
const value = organization.repositories.totalCount const value = organization.repositories.totalCount
const unlock = organization.repositories.nodes?.shift() const unlock = organization.repositories.nodes?.shift()
list.push({ list.push({
title:"Developers", title: "Developers",
text:`Published ${value} public repositor${imports.s(value, "y")}`, text: `Published ${value} public repositor${imports.s(value, "y")}`,
icon:'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#primary"><path d="M20 24l-3.397 3.398a.85.85 0 000 1.203L20.002 32M37.015 24l3.399 3.398a.85.85 0 010 1.203L37.014 32" stroke-linejoin="round"/><path d="M31.029 21.044L25.976 35.06"/></g><path stroke="#secondary" stroke-linejoin="round" d="M23.018 10h8.984M26 47h5M8 16h16m9 0h15.725M8 41h13"/><path d="M5.027 34.998c.673 2.157 1.726 4.396 2.81 6.02m43.38-19.095C50.7 19.921 49.866 17.796 48.79 16" stroke="#secondary"/><path stroke="#primary" stroke-linejoin="round" d="M26 41h17"/><path d="M7.183 16C5.186 19.582 4 23.619 4 28M42.608 47.02c2.647-1.87 5.642-5.448 7.295-9.18C51.52 34.191 52.071 30.323 52 28" stroke="#primary"/><path stroke="#primary" stroke-linejoin="round" d="M7.226 16H28M13.343 47H21"/><path d="M13.337 47.01a24.364 24.364 0 006.19 3.45 24.527 24.527 0 007.217 1.505c2.145.108 4.672-.05 7.295-.738" stroke="#primary"/><path stroke="#primary" stroke-linejoin="round" d="M36 47h6.647M12 10h6M37 10h6.858"/><path d="M43.852 10c-4.003-3.667-9.984-6.054-16.047-6-2.367.021-4.658.347-6.81 1.045" stroke="#primary"/><path stroke="#secondary" stroke-linejoin="round" d="M5.041 35h4.962M47 22h4.191"/></g>', icon:
'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#primary"><path d="M20 24l-3.397 3.398a.85.85 0 000 1.203L20.002 32M37.015 24l3.399 3.398a.85.85 0 010 1.203L37.014 32" stroke-linejoin="round"/><path d="M31.029 21.044L25.976 35.06"/></g><path stroke="#secondary" stroke-linejoin="round" d="M23.018 10h8.984M26 47h5M8 16h16m9 0h15.725M8 41h13"/><path d="M5.027 34.998c.673 2.157 1.726 4.396 2.81 6.02m43.38-19.095C50.7 19.921 49.866 17.796 48.79 16" stroke="#secondary"/><path stroke="#primary" stroke-linejoin="round" d="M26 41h17"/><path d="M7.183 16C5.186 19.582 4 23.619 4 28M42.608 47.02c2.647-1.87 5.642-5.448 7.295-9.18C51.52 34.191 52.071 30.323 52 28" stroke="#primary"/><path stroke="#primary" stroke-linejoin="round" d="M7.226 16H28M13.343 47H21"/><path d="M13.337 47.01a24.364 24.364 0 006.19 3.45 24.527 24.527 0 007.217 1.505c2.145.108 4.672-.05 7.295-.738" stroke="#primary"/><path stroke="#primary" stroke-linejoin="round" d="M36 47h6.647M12 10h6M37 10h6.858"/><path d="M43.852 10c-4.003-3.667-9.984-6.054-16.047-6-2.367.021-4.658.347-6.81 1.045" stroke="#primary"/><path stroke="#secondary" stroke-linejoin="round" d="M5.041 35h4.962M47 22h4.191"/></g>',
...rank(value, [1, 50, 100, 200, 300]), ...rank(value, [1, 50, 100, 200, 300]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
leaderboard:leaderboard({user:ranks.created_rank.userCount, requirement:scores.created >= requirements.created, type:"users"}), leaderboard: leaderboard({user: ranks.created_rank.userCount, requirement: scores.created >= requirements.created, type: "users"}),
}) })
} }
@@ -26,12 +27,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const value = organization.forks.totalCount const value = organization.forks.totalCount
const unlock = organization.forks.nodes?.shift() const unlock = organization.forks.nodes?.shift()
list.push({ list.push({
title:"Forkers", title: "Forkers",
text:`Forked ${value} public repositor${imports.s(value, "y")}`, text: `Forked ${value} public repositor${imports.s(value, "y")}`,
icon:'<g stroke-width="2" fill="none" fill-rule="evenodd"><path d="M37.303 21.591a5.84 5.84 0 00-1.877-1.177 6.138 6.138 0 00-4.432 0 5.822 5.822 0 00-1.879 1.177L28 22.638l-1.115-1.047c-1.086-1.018-2.559-1.59-4.094-1.59-1.536 0-3.008.572-4.094 1.59-1.086 1.02-1.696 2.4-1.696 3.84 0 1.441.61 2.823 1.696 3.841l1.115 1.046L28 38l8.189-7.682 1.115-1.046a5.422 5.422 0 001.256-1.761 5.126 5.126 0 000-4.157 5.426 5.426 0 00-1.256-1.763z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.967 42.705A18.922 18.922 0 0028 47a18.92 18.92 0 0011.076-3.56m-.032-30.902A18.914 18.914 0 0028 9c-4.09 0-7.876 1.292-10.976 3.49" stroke="#secondary" stroke-linecap="round"/><g transform="translate(7 10)" stroke="#primary"><path d="M6 0v7c0 2.21-1.343 3-3 3s-3-.79-3-3V0" stroke-linecap="round" stroke-linejoin="round"/><path stroke-linecap="round" d="M3 0v19.675"/><rect stroke-linejoin="round" x="1" y="20" width="4" height="16" rx="2"/></g><g transform="translate(43 10)" stroke="#primary"><path stroke-linecap="round" d="M2 15.968v3.674"/><path d="M4 15.642H0L.014 4.045A4.05 4.05 0 014.028 0L4 15.642z" stroke-linecap="round" stroke-linejoin="round"/><rect stroke-linejoin="round" y="19.968" width="4" height="16" rx="2"/></g><path d="M41.364 8.062A23.888 23.888 0 0028 4a23.89 23.89 0 00-11.95 3.182M4.75 22.021A24.045 24.045 0 004 28c0 1.723.182 3.404.527 5.024m10.195 14.971A23.888 23.888 0 0028 52c4.893 0 9.444-1.464 13.239-3.979m9-10.98A23.932 23.932 0 0052 28c0-2.792-.477-5.472-1.353-7.964" stroke="#secondary" stroke-linecap="round"/></g>', icon:
'<g stroke-width="2" fill="none" fill-rule="evenodd"><path d="M37.303 21.591a5.84 5.84 0 00-1.877-1.177 6.138 6.138 0 00-4.432 0 5.822 5.822 0 00-1.879 1.177L28 22.638l-1.115-1.047c-1.086-1.018-2.559-1.59-4.094-1.59-1.536 0-3.008.572-4.094 1.59-1.086 1.02-1.696 2.4-1.696 3.84 0 1.441.61 2.823 1.696 3.841l1.115 1.046L28 38l8.189-7.682 1.115-1.046a5.422 5.422 0 001.256-1.761 5.126 5.126 0 000-4.157 5.426 5.426 0 00-1.256-1.763z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.967 42.705A18.922 18.922 0 0028 47a18.92 18.92 0 0011.076-3.56m-.032-30.902A18.914 18.914 0 0028 9c-4.09 0-7.876 1.292-10.976 3.49" stroke="#secondary" stroke-linecap="round"/><g transform="translate(7 10)" stroke="#primary"><path d="M6 0v7c0 2.21-1.343 3-3 3s-3-.79-3-3V0" stroke-linecap="round" stroke-linejoin="round"/><path stroke-linecap="round" d="M3 0v19.675"/><rect stroke-linejoin="round" x="1" y="20" width="4" height="16" rx="2"/></g><g transform="translate(43 10)" stroke="#primary"><path stroke-linecap="round" d="M2 15.968v3.674"/><path d="M4 15.642H0L.014 4.045A4.05 4.05 0 014.028 0L4 15.642z" stroke-linecap="round" stroke-linejoin="round"/><rect stroke-linejoin="round" y="19.968" width="4" height="16" rx="2"/></g><path d="M41.364 8.062A23.888 23.888 0 0028 4a23.89 23.89 0 00-11.95 3.182M4.75 22.021A24.045 24.045 0 004 28c0 1.723.182 3.404.527 5.024m10.195 14.971A23.888 23.888 0 0028 52c4.893 0 9.444-1.464 13.239-3.979m9-10.98A23.932 23.932 0 0052 28c0-2.792-.477-5.472-1.353-7.964" stroke="#secondary" stroke-linecap="round"/></g>',
...rank(value, [1, 10, 30, 50, 100]), ...rank(value, [1, 10, 30, 50, 100]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -41,12 +43,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = organization.projects.nodes?.shift() const unlock = organization.projects.nodes?.shift()
list.push({ list.push({
title:"Managers", title: "Managers",
text:`Created ${value} user project${imports.s(value)}`, text: `Created ${value} user project${imports.s(value)}`,
icon:'<g stroke-width="2" fill="none" fill-rule="evenodd"><path d="M29 16V8.867C29 7.705 29.627 7 30.692 7h18.616C50.373 7 51 7.705 51 8.867v38.266C51 48.295 50.373 49 49.308 49H30.692C29.627 49 29 48.295 29 47.133V39m-4-23V9c0-1.253-.737-2-2-2H7c-1.263 0-2 .747-2 2v34c0 1.253.737 2 2 2h16c1.263 0 2-.747 2-2v-4" stroke="#secondary" stroke-linecap="round"/><path stroke="#secondary" d="M51.557 12.005h-22M5 12.005h21"/><path d="M14 33V22c0-1.246.649-2 1.73-2h28.54c1.081 0 1.73.754 1.73 2v11c0 1.246-.649 2-1.73 2H15.73c-1.081 0-1.73-.754-1.73-2z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path d="M19 29v-3c0-.508.492-1 1-1h3c.508 0 1 .492 1 1v3c0 .508-.492 1-1 1h-3c-.508-.082-1-.492-1-1z" stroke="#primary"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M28.996 27.998h12M9.065 20.04a7.062 7.062 0 00-.023 1.728m.775 2.517c.264.495.584.954.954 1.369"/></g>', icon:
'<g stroke-width="2" fill="none" fill-rule="evenodd"><path d="M29 16V8.867C29 7.705 29.627 7 30.692 7h18.616C50.373 7 51 7.705 51 8.867v38.266C51 48.295 50.373 49 49.308 49H30.692C29.627 49 29 48.295 29 47.133V39m-4-23V9c0-1.253-.737-2-2-2H7c-1.263 0-2 .747-2 2v34c0 1.253.737 2 2 2h16c1.263 0 2-.747 2-2v-4" stroke="#secondary" stroke-linecap="round"/><path stroke="#secondary" d="M51.557 12.005h-22M5 12.005h21"/><path d="M14 33V22c0-1.246.649-2 1.73-2h28.54c1.081 0 1.73.754 1.73 2v11c0 1.246-.649 2-1.73 2H15.73c-1.081 0-1.73-.754-1.73-2z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path d="M19 29v-3c0-.508.492-1 1-1h3c.508 0 1 .492 1 1v3c0 .508-.492 1-1 1h-3c-.508-.082-1-.492-1-1z" stroke="#primary"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M28.996 27.998h12M9.065 20.04a7.062 7.062 0 00-.023 1.728m.775 2.517c.264.495.584.954.954 1.369"/></g>',
...rank(value, [1, 2, 4, 8, 10]), ...rank(value, [1, 2, 4, 8, 10]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -56,12 +59,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = organization.packages.nodes?.shift() const unlock = organization.packages.nodes?.shift()
list.push({ list.push({
title:"Packagers", title: "Packagers",
text:`Created ${value} package${imports.s(value)}`, text: `Created ${value} package${imports.s(value)}`,
icon:'<g fill="none"><path fill="#secondary" d="M28.53 27.64l-11.2 6.49V21.15l11.23-6.48z"/><path d="M40.4 34.84c-.17 0-.34-.04-.5-.13l-11.24-6.44a.99.99 0 01-.37-1.36.99.99 0 011.36-.37l11.24 6.44c.48.27.65.89.37 1.36-.17.32-.51.5-.86.5z" fill="#primary"/><path d="M29.16 28.4c-.56 0-1-.45-1-1.01l.08-12.47c0-.55.49-1 1.01-.99.55 0 1 .45.99 1.01l-.08 12.47c0 .55-.45.99-1 .99z" fill="#primary"/><path d="M18.25 34.65a.996.996 0 01-.5-1.86l10.91-6.25a.997.997 0 11.99 1.73l-10.91 6.25c-.15.09-.32.13-.49.13z" fill="#primary"/><path d="M29.19 41.37c-.17 0-.35-.04-.5-.13l-11.23-6.49c-.31-.18-.5-.51-.5-.87V20.91c0-.36.19-.69.5-.87l11.23-6.49c.31-.18.69-.18 1 0l11.23 6.49c.31.18.5.51.5.87v12.97c0 .36-.19.69-.5.87l-11.23 6.49c-.15.08-.32.13-.5.13zm-10.23-8.06l10.23 5.91 10.23-5.91V21.49l-10.23-5.91-10.23 5.91v11.82zM40.5 11.02c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm-23.19 4.36c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.42 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm23.37 43.8c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.42 3.18-3.18 3.18zm0-4.35c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm-23.06 4.11c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zM6.18 30.72C4.43 30.72 3 29.29 3 27.54c0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm45.64 4.36c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18z" fill="#primary"/><path d="M29.1 10.21c-.55 0-1-.45-1-1V3.52c0-.55.45-1 1-1s1 .45 1 1v5.69c0 .56-.45 1-1 1zM7.44 20.95c-.73 0-1.32-.59-1.32-1.32v-5.38l4.66-2.69c.63-.37 1.44-.15 1.8.48.36.63.15 1.44-.48 1.8l-3.34 1.93v3.86c0 .73-.59 1.32-1.32 1.32zm4 22.68c-.22 0-.45-.06-.66-.18l-4.66-2.69v-5.38c0-.73.59-1.32 1.32-1.32.73 0 1.32.59 1.32 1.32v3.86l3.34 1.93c.63.36.85 1.17.48 1.8-.24.42-.68.66-1.14.66zm17.64 10.39l-4.66-2.69c-.63-.36-.85-1.17-.48-1.8.36-.63 1.17-.85 1.8-.48l3.34 1.93 3.34-1.93a1.32 1.32 0 011.8.48c.36.63.15 1.44-.48 1.8l-4.66 2.69zm17.64-10.39a1.32 1.32 0 01-.66-2.46l3.34-1.93v-3.86c0-.73.59-1.32 1.32-1.32.73 0 1.32.59 1.32 1.32v5.38l-4.66 2.69c-.21.12-.44.18-.66.18zm4-22.68c-.73 0-1.32-.59-1.32-1.32v-3.86l-3.34-1.93c-.63-.36-.85-1.17-.48-1.8.36-.63 1.17-.85 1.8-.48l4.66 2.69v5.38c0 .73-.59 1.32-1.32 1.32z" fill="#secondary"/><path d="M33.08 6.15c-.22 0-.45-.06-.66-.18l-3.34-1.93-3.34 1.93c-.63.36-1.44.15-1.8-.48a1.32 1.32 0 01.48-1.8L29.08 1l4.66 2.69c.63.36.85 1.17.48 1.8a1.3 1.3 0 01-1.14.66zm-3.99 47.3c-.55 0-1-.45-1-1v-7.13c0-.55.45-1 1-1s1 .45 1 1v7.13c0 .55-.44 1-1 1zM13.86 19.71c-.17 0-.34-.04-.5-.13L7.2 16a1 1 0 011-1.73l6.17 3.58c.48.28.64.89.36 1.37-.19.31-.52.49-.87.49zm36.63 21.23c-.17 0-.34-.04-.5-.13l-6.17-3.57a.998.998 0 01-.36-1.37c.28-.48.89-.64 1.37-.36L51 39.08c.48.28.64.89.36 1.37-.19.31-.52.49-.87.49zM44.06 19.8c-.35 0-.68-.18-.87-.5-.28-.48-.11-1.09.36-1.37l6.17-3.57c.48-.28 1.09-.11 1.37.36.28.48.11 1.09-.36 1.37l-6.17 3.57c-.16.1-.33.14-.5.14zM7.43 41.03c-.35 0-.68-.18-.87-.5-.28-.48-.11-1.09.36-1.37l6.17-3.57c.48-.28 1.09-.11 1.37.36.28.48.11 1.09-.36 1.37l-6.17 3.57c-.15.09-.33.14-.5.14z" fill="#secondary"/></g>', icon:
'<g fill="none"><path fill="#secondary" d="M28.53 27.64l-11.2 6.49V21.15l11.23-6.48z"/><path d="M40.4 34.84c-.17 0-.34-.04-.5-.13l-11.24-6.44a.99.99 0 01-.37-1.36.99.99 0 011.36-.37l11.24 6.44c.48.27.65.89.37 1.36-.17.32-.51.5-.86.5z" fill="#primary"/><path d="M29.16 28.4c-.56 0-1-.45-1-1.01l.08-12.47c0-.55.49-1 1.01-.99.55 0 1 .45.99 1.01l-.08 12.47c0 .55-.45.99-1 .99z" fill="#primary"/><path d="M18.25 34.65a.996.996 0 01-.5-1.86l10.91-6.25a.997.997 0 11.99 1.73l-10.91 6.25c-.15.09-.32.13-.49.13z" fill="#primary"/><path d="M29.19 41.37c-.17 0-.35-.04-.5-.13l-11.23-6.49c-.31-.18-.5-.51-.5-.87V20.91c0-.36.19-.69.5-.87l11.23-6.49c.31-.18.69-.18 1 0l11.23 6.49c.31.18.5.51.5.87v12.97c0 .36-.19.69-.5.87l-11.23 6.49c-.15.08-.32.13-.5.13zm-10.23-8.06l10.23 5.91 10.23-5.91V21.49l-10.23-5.91-10.23 5.91v11.82zM40.5 11.02c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm-23.19 4.36c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.42 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm23.37 43.8c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.42 3.18-3.18 3.18zm0-4.35c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm-23.06 4.11c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zM6.18 30.72C4.43 30.72 3 29.29 3 27.54c0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm45.64 4.36c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18z" fill="#primary"/><path d="M29.1 10.21c-.55 0-1-.45-1-1V3.52c0-.55.45-1 1-1s1 .45 1 1v5.69c0 .56-.45 1-1 1zM7.44 20.95c-.73 0-1.32-.59-1.32-1.32v-5.38l4.66-2.69c.63-.37 1.44-.15 1.8.48.36.63.15 1.44-.48 1.8l-3.34 1.93v3.86c0 .73-.59 1.32-1.32 1.32zm4 22.68c-.22 0-.45-.06-.66-.18l-4.66-2.69v-5.38c0-.73.59-1.32 1.32-1.32.73 0 1.32.59 1.32 1.32v3.86l3.34 1.93c.63.36.85 1.17.48 1.8-.24.42-.68.66-1.14.66zm17.64 10.39l-4.66-2.69c-.63-.36-.85-1.17-.48-1.8.36-.63 1.17-.85 1.8-.48l3.34 1.93 3.34-1.93a1.32 1.32 0 011.8.48c.36.63.15 1.44-.48 1.8l-4.66 2.69zm17.64-10.39a1.32 1.32 0 01-.66-2.46l3.34-1.93v-3.86c0-.73.59-1.32 1.32-1.32.73 0 1.32.59 1.32 1.32v5.38l-4.66 2.69c-.21.12-.44.18-.66.18zm4-22.68c-.73 0-1.32-.59-1.32-1.32v-3.86l-3.34-1.93c-.63-.36-.85-1.17-.48-1.8.36-.63 1.17-.85 1.8-.48l4.66 2.69v5.38c0 .73-.59 1.32-1.32 1.32z" fill="#secondary"/><path d="M33.08 6.15c-.22 0-.45-.06-.66-.18l-3.34-1.93-3.34 1.93c-.63.36-1.44.15-1.8-.48a1.32 1.32 0 01.48-1.8L29.08 1l4.66 2.69c.63.36.85 1.17.48 1.8a1.3 1.3 0 01-1.14.66zm-3.99 47.3c-.55 0-1-.45-1-1v-7.13c0-.55.45-1 1-1s1 .45 1 1v7.13c0 .55-.44 1-1 1zM13.86 19.71c-.17 0-.34-.04-.5-.13L7.2 16a1 1 0 011-1.73l6.17 3.58c.48.28.64.89.36 1.37-.19.31-.52.49-.87.49zm36.63 21.23c-.17 0-.34-.04-.5-.13l-6.17-3.57a.998.998 0 01-.36-1.37c.28-.48.89-.64 1.37-.36L51 39.08c.48.28.64.89.36 1.37-.19.31-.52.49-.87.49zM44.06 19.8c-.35 0-.68-.18-.87-.5-.28-.48-.11-1.09.36-1.37l6.17-3.57c.48-.28 1.09-.11 1.37.36.28.48.11 1.09-.36 1.37l-6.17 3.57c-.16.1-.33.14-.5.14zM7.43 41.03c-.35 0-.68-.18-.87-.5-.28-.48-.11-1.09.36-1.37l6.17-3.57c.48-.28 1.09-.11 1.37.36.28.48.11 1.09-.36 1.37l-6.17 3.57c-.15.09-.33.14-.5.14z" fill="#secondary"/></g>',
...rank(value, [1, 20, 50, 100, 250]), ...rank(value, [1, 20, 50, 100, 250]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -71,13 +75,14 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = null const unlock = null
list.push({ list.push({
title:"Maintainers", title: "Maintainers",
text:`Maintaining a repository with ${value} star${imports.s(value)}`, text: `Maintaining a repository with ${value} star${imports.s(value)}`,
icon:'<g transform="translate(4 4)" fill="none" fill-rule="evenodd"><path d="M39 15h.96l4.038 3-.02-3H45a2 2 0 002-2V3a2 2 0 00-2-2H31a2 2 0 00-2 2v4.035" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M36 5.014l-3 3 3 3M40 5.014l3 3-3 3"/><path d="M6 37a1 1 0 110 2 1 1 0 010-2m7 0a1 1 0 110 2 1 1 0 010-2m-2.448 1a1 1 0 11-2 0 1 1 0 012 0z" fill="#primary"/><path d="M1.724 15.05A23.934 23.934 0 000 24c0 .686.029 1.366.085 2.037m19.92 21.632c1.3.218 2.634.331 3.995.331a23.92 23.92 0 009.036-1.76m13.207-13.21A23.932 23.932 0 0048 24c0-1.363-.114-2.7-.332-4M25.064.022a23.932 23.932 0 00-10.073 1.725" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M19 42.062V43a2 2 0 01-2 2H9.04l-4.038 3 .02-3H3a2 2 0 01-2-2V33a2 2 0 012-2h4.045" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M6 0a6 6 0 110 12A6 6 0 016 0z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" d="M6 3v6M3 6h6"/><path d="M42 36a6 6 0 110 12 6 6 0 010-12z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M44.338 40.663l-3.336 3.331-1.692-1.686M31 31c-.716-2.865-3.578-5-7-5-3.423 0-6.287 2.14-7 5"/><path d="M24 16a5 5 0 110 10 5 5 0 010-10z" stroke="#primary" stroke-width="2" stroke-linecap="round"/><circle stroke="#primary" stroke-width="2" cx="24" cy="24" r="14"/></g>', icon:
'<g transform="translate(4 4)" fill="none" fill-rule="evenodd"><path d="M39 15h.96l4.038 3-.02-3H45a2 2 0 002-2V3a2 2 0 00-2-2H31a2 2 0 00-2 2v4.035" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M36 5.014l-3 3 3 3M40 5.014l3 3-3 3"/><path d="M6 37a1 1 0 110 2 1 1 0 010-2m7 0a1 1 0 110 2 1 1 0 010-2m-2.448 1a1 1 0 11-2 0 1 1 0 012 0z" fill="#primary"/><path d="M1.724 15.05A23.934 23.934 0 000 24c0 .686.029 1.366.085 2.037m19.92 21.632c1.3.218 2.634.331 3.995.331a23.92 23.92 0 009.036-1.76m13.207-13.21A23.932 23.932 0 0048 24c0-1.363-.114-2.7-.332-4M25.064.022a23.932 23.932 0 00-10.073 1.725" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M19 42.062V43a2 2 0 01-2 2H9.04l-4.038 3 .02-3H3a2 2 0 01-2-2V33a2 2 0 012-2h4.045" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M6 0a6 6 0 110 12A6 6 0 016 0z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" d="M6 3v6M3 6h6"/><path d="M42 36a6 6 0 110 12 6 6 0 010-12z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M44.338 40.663l-3.336 3.331-1.692-1.686M31 31c-.716-2.865-3.578-5-7-5-3.423 0-6.287 2.14-7 5"/><path d="M24 16a5 5 0 110 10 5 5 0 010-10z" stroke="#primary" stroke-width="2" stroke-linecap="round"/><circle stroke="#primary" stroke-width="2" cx="24" cy="24" r="14"/></g>',
...rank(value, [1, 5000, 10000, 30000, 50000]), ...rank(value, [1, 5000, 10000, 30000, 50000]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
leaderboard:leaderboard({user:ranks.repo_rank.repositoryCount, requirement:scores.stars >= requirements.stars, type:"repositories"}), leaderboard: leaderboard({user: ranks.repo_rank.repositoryCount, requirement: scores.stars >= requirements.stars, type: "repositories"}),
}) })
} }
@@ -86,28 +91,30 @@ export default async function({list, login, data, computed, imports, graphql, qu
const value = Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount)) const value = Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount))
const unlock = null const unlock = null
list.push({ list.push({
title:"Inspirers", title: "Inspirers",
text:`Maintaining or created a repository which has been forked ${value} time${imports.s(value)}`, text: `Maintaining or created a repository which has been forked ${value} time${imports.s(value)}`,
icon:'<g transform="translate(4 4)" fill="none" fill-rule="evenodd"><path d="M20.065 47.122c.44-.525.58-1.448.58-1.889 0-2.204-1.483-3.967-3.633-4.187.447-1.537.58-2.64.397-3.31-.25-.92-.745-1.646-1.409-2.235m-5.97-7.157c.371-.254.911-.748 1.62-1.48a8.662 8.662 0 001.432-2.366M47 22h-7c-1.538 0-2.749-.357-4-1h-5c-1.789.001-3-1.3-3-2.955 0-1.656 1.211-3.04 3-3.045h2c.027-1.129.513-2.17 1-3m3.082 32.004C34.545 43.028 34.02 40.569 34 39v-1h-1c-2.603-.318-5-2.913-5-5.997S30.397 26 33 26h9c2.384 0 4.326 1.024 5.27 3" stroke="#secondary" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><g transform="translate(36)" stroke="#primary" stroke-width="2"><path fill="#primary" stroke-linecap="round" stroke-linejoin="round" d="M5.395 5.352L6.009 4l.598 1.348L8 5.408l-1.067 1.12.425 1.47-1.356-.908-1.35.91.404-1.469L4 5.41z"/><circle cx="6" cy="6" r="6"/></g><g transform="translate(0 31)" stroke="#primary" stroke-width="2"><circle cx="6" cy="6" r="6"/><g stroke-linecap="round"><path d="M6 4v4M4 6h4"/></g></g><circle stroke="#primary" stroke-width="2" cx="10.5" cy="10.5" r="10.5"/><g stroke-linecap="round"><path d="M32.01 1.37A23.96 23.96 0 0024 0c-.999 0-1.983.061-2.95.18M.32 20.072a24.21 24.21 0 00.015 7.948M12.42 45.025A23.892 23.892 0 0024 48c13.255 0 24-10.745 24-24 0-2.811-.483-5.51-1.371-8.016" stroke="#secondary" stroke-width="2"/><path stroke="#primary" stroke-width="2" d="M8.999 7.151v5.865"/><path d="M9 3a2 2 0 110 4 2 2 0 010-4zm0 10.8a2 2 0 11-.001 4 2 2 0 01.001-4z" stroke="#primary" stroke-width="1.8"/><path d="M9.622 11.838c.138-.007.989.119 1.595-.05.607-.169 1.584-.539 1.829-1.337" stroke="#primary" stroke-width="2"/><path d="M14.8 7.202a2 2 0 110 4 2 2 0 010-4z" stroke="#primary" stroke-width="1.8"/></g></g>', icon:
'<g transform="translate(4 4)" fill="none" fill-rule="evenodd"><path d="M20.065 47.122c.44-.525.58-1.448.58-1.889 0-2.204-1.483-3.967-3.633-4.187.447-1.537.58-2.64.397-3.31-.25-.92-.745-1.646-1.409-2.235m-5.97-7.157c.371-.254.911-.748 1.62-1.48a8.662 8.662 0 001.432-2.366M47 22h-7c-1.538 0-2.749-.357-4-1h-5c-1.789.001-3-1.3-3-2.955 0-1.656 1.211-3.04 3-3.045h2c.027-1.129.513-2.17 1-3m3.082 32.004C34.545 43.028 34.02 40.569 34 39v-1h-1c-2.603-.318-5-2.913-5-5.997S30.397 26 33 26h9c2.384 0 4.326 1.024 5.27 3" stroke="#secondary" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><g transform="translate(36)" stroke="#primary" stroke-width="2"><path fill="#primary" stroke-linecap="round" stroke-linejoin="round" d="M5.395 5.352L6.009 4l.598 1.348L8 5.408l-1.067 1.12.425 1.47-1.356-.908-1.35.91.404-1.469L4 5.41z"/><circle cx="6" cy="6" r="6"/></g><g transform="translate(0 31)" stroke="#primary" stroke-width="2"><circle cx="6" cy="6" r="6"/><g stroke-linecap="round"><path d="M6 4v4M4 6h4"/></g></g><circle stroke="#primary" stroke-width="2" cx="10.5" cy="10.5" r="10.5"/><g stroke-linecap="round"><path d="M32.01 1.37A23.96 23.96 0 0024 0c-.999 0-1.983.061-2.95.18M.32 20.072a24.21 24.21 0 00.015 7.948M12.42 45.025A23.892 23.892 0 0024 48c13.255 0 24-10.745 24-24 0-2.811-.483-5.51-1.371-8.016" stroke="#secondary" stroke-width="2"/><path stroke="#primary" stroke-width="2" d="M8.999 7.151v5.865"/><path d="M9 3a2 2 0 110 4 2 2 0 010-4zm0 10.8a2 2 0 11-.001 4 2 2 0 01.001-4z" stroke="#primary" stroke-width="1.8"/><path d="M9.622 11.838c.138-.007.989.119 1.595-.05.607-.169 1.584-.539 1.829-1.337" stroke="#primary" stroke-width="2"/><path d="M14.8 7.202a2 2 0 110 4 2 2 0 010-4z" stroke="#primary" stroke-width="1.8"/></g></g>',
...rank(value, [1, 500, 1000, 3000, 5000]), ...rank(value, [1, 500, 1000, 3000, 5000]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
leaderboard:leaderboard({user:ranks.forks_rank.repositoryCount, requirement:scores.forks >= requirements.forks, type:"repositories"}), leaderboard: leaderboard({user: ranks.forks_rank.repositoryCount, requirement: scores.forks >= requirements.forks, type: "repositories"}),
}) })
} }
//Polyglots //Polyglots
{ {
const value = new Set(data.user.repositories.nodes.flatMap(repository => repository.languages.edges.map(({node:{name}}) => name))).size const value = new Set(data.user.repositories.nodes.flatMap(repository => repository.languages.edges.map(({node: {name}}) => name))).size
const unlock = null const unlock = null
list.push({ list.push({
title:"Polyglots", title: "Polyglots",
text:`Using ${value} different programming language${imports.s(value)}`, text: `Using ${value} different programming language${imports.s(value)}`,
icon:'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><path d="M17.135 7.988l-3.303.669a2 2 0 00-1.586 2.223l4.708 35.392a1.498 1.498 0 01-1.162 1.66 1.523 1.523 0 01-1.775-1.01L4.951 19.497a2 2 0 011.215-2.507l2.946-1.072" stroke="#secondary" stroke-linejoin="round"/><path d="M36.8 48H23a2 2 0 01-2-2V7a2 2 0 012-2h26a2 2 0 012 2v32.766" stroke="#primary"/><path d="M29 20.955l-3.399 3.399a.85.85 0 000 1.202l3.399 3.4M43.014 20.955l3.399 3.399a.85.85 0 010 1.202l-3.4 3.4" stroke="#primary" stroke-linejoin="round"/><path stroke="#primary" d="M38.526 18l-5.053 14.016"/><path d="M44 36a8 8 0 110 16 8 8 0 010-16z" stroke="#primary" stroke-linejoin="round"/><path d="M43.068 40.749l3.846 2.396a1 1 0 01-.006 1.7l-3.846 2.36a1 1 0 01-1.523-.853v-4.755a1 1 0 011.529-.848z" stroke="#primary" stroke-linejoin="round"/></g>', icon:
'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><path d="M17.135 7.988l-3.303.669a2 2 0 00-1.586 2.223l4.708 35.392a1.498 1.498 0 01-1.162 1.66 1.523 1.523 0 01-1.775-1.01L4.951 19.497a2 2 0 011.215-2.507l2.946-1.072" stroke="#secondary" stroke-linejoin="round"/><path d="M36.8 48H23a2 2 0 01-2-2V7a2 2 0 012-2h26a2 2 0 012 2v32.766" stroke="#primary"/><path d="M29 20.955l-3.399 3.399a.85.85 0 000 1.202l3.399 3.4M43.014 20.955l3.399 3.399a.85.85 0 010 1.202l-3.4 3.4" stroke="#primary" stroke-linejoin="round"/><path stroke="#primary" d="M38.526 18l-5.053 14.016"/><path d="M44 36a8 8 0 110 16 8 8 0 010-16z" stroke="#primary" stroke-linejoin="round"/><path d="M43.068 40.749l3.846 2.396a1 1 0 01-.006 1.7l-3.846 2.36a1 1 0 01-1.523-.853v-4.755a1 1 0 011.529-.848z" stroke="#primary" stroke-linejoin="round"/></g>',
...rank(value, [1, 8, 16, 32, 64]), ...rank(value, [1, 8, 16, 32, 64]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -117,12 +124,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = null const unlock = null
list.push({ list.push({
title:"Sponsors", title: "Sponsors",
text:`Sponsoring ${value} user${imports.s(value)} or organization${imports.s(value)}`, text: `Sponsoring ${value} user${imports.s(value)} or organization${imports.s(value)}`,
icon:'<g xmlns="http://www.w3.org/2000/svg" fill="none" fill-rule="evenodd"><path d="M24 32c.267-1.727 1.973-3 4-3 2.08 0 3.787 1.318 4 3m-4-9a3 3 0 110 6 3 3 0 010-6z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M28 18c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M46.138 15c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C41.347 15 41 16.117 41 17.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005zm-31-5c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C10.347 10 10 11.117 10 12.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005zm6 32c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C16.347 42 16 43.117 16 44.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005z" fill="#secondary"/><path d="M8.003 29a3 3 0 110 6 3 3 0 010-6zM32.018 5.005a3 3 0 110 6 3 3 0 010-6z" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path stroke="#secondary" stroke-width="2" d="M29.972 18.026L31.361 11M18.063 29.987l-7.004 1.401"/><path d="M22.604 11.886l.746 2.164m-9.313 9.296l-2.156-.712" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M21.304 9a1 1 0 100-2 1 1 0 000 2zM8.076 22.346a1 1 0 100-2 1 1 0 000 2z" fill="#primary"/><path d="M33.267 44.17l-.722-2.146m9.38-9.206l2.147.743" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M34.544 49.031a1 1 0 100-2 1 1 0 000 2zm13.314-13.032a1 1 0 100-2 1 1 0 000 2z" fill="#primary"/><path d="M48.019 51.004a3 3 0 100-6 3 3 0 000 6zM35.194 35.33l10.812 11.019" stroke="#secondary" stroke-width="2"/></g>', icon:
'<g xmlns="http://www.w3.org/2000/svg" fill="none" fill-rule="evenodd"><path d="M24 32c.267-1.727 1.973-3 4-3 2.08 0 3.787 1.318 4 3m-4-9a3 3 0 110 6 3 3 0 010-6z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M28 18c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M46.138 15c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C41.347 15 41 16.117 41 17.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005zm-31-5c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C10.347 10 10 11.117 10 12.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005zm6 32c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C16.347 42 16 43.117 16 44.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005z" fill="#secondary"/><path d="M8.003 29a3 3 0 110 6 3 3 0 010-6zM32.018 5.005a3 3 0 110 6 3 3 0 010-6z" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path stroke="#secondary" stroke-width="2" d="M29.972 18.026L31.361 11M18.063 29.987l-7.004 1.401"/><path d="M22.604 11.886l.746 2.164m-9.313 9.296l-2.156-.712" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M21.304 9a1 1 0 100-2 1 1 0 000 2zM8.076 22.346a1 1 0 100-2 1 1 0 000 2z" fill="#primary"/><path d="M33.267 44.17l-.722-2.146m9.38-9.206l2.147.743" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M34.544 49.031a1 1 0 100-2 1 1 0 000 2zm13.314-13.032a1 1 0 100-2 1 1 0 000 2z" fill="#primary"/><path d="M48.019 51.004a3 3 0 100-6 3 3 0 000 6zM35.194 35.33l10.812 11.019" stroke="#secondary" stroke-width="2"/></g>',
...rank(value, [1, 5, 10, 20, 50]), ...rank(value, [1, 5, 10, 20, 50]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -132,27 +140,29 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = null const unlock = null
list.push({ list.push({
title:"Organization", title: "Organization",
text:`Has ${value} member${imports.s(value)}`, text: `Has ${value} member${imports.s(value)}`,
icon:'<g xmlns="http://www.w3.org/2000/svg" fill="none" fill-rule="evenodd"><path d="M6 42c.45-3.415 3.34-6 7-6 1.874 0 3.752.956 5 3m-6-13a5 5 0 110 10 5 5 0 010-10zm38 16c-.452-3.415-3.34-6-7-6-1.874 0-3.752.956-5 3m6-13a5 5 0 100 10 5 5 0 000-10z" stroke="#primary" stroke-width="2" stroke-linecap="round"/><path d="M37 51c-.92-4.01-4.6-7-9-7-4.401 0-8.083 2.995-9 7" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M28.01 31.004a6.5 6.5 0 110 13 6.5 6.5 0 010-13z" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M28 14.011a5 5 0 11-5 4.998 5 5 0 015-4.998z" stroke="#primary" stroke-width="2" stroke-linecap="round"/><path d="M22 26c1.558-1.25 3.665-2 6-2 2.319 0 4.439.761 6 2" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M51 9V8c0-1.3-1.574-3-3-3h-8c-1.426 0-3 1.7-3 3v13l4-4h6c2.805-.031 4-1.826 4-4V9zM5 9V8c0-1.3 1.574-3 3-3h8c1.426 0 3 1.7 3 3v13l-4-4H9c-2.805-.031-4-1.826-4-4V9z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M43 11a1 1 0 11-2 0 1 1 0 012 0zm4 0a1 1 0 11-2 0 1 1 0 012 0zm-36 0a1 1 0 11-2 0 1 1 0 012 0zm4 0a1 1 0 11-2 0 1 1 0 012 0z" fill="#secondary"/></g>', icon:
'<g xmlns="http://www.w3.org/2000/svg" fill="none" fill-rule="evenodd"><path d="M6 42c.45-3.415 3.34-6 7-6 1.874 0 3.752.956 5 3m-6-13a5 5 0 110 10 5 5 0 010-10zm38 16c-.452-3.415-3.34-6-7-6-1.874 0-3.752.956-5 3m6-13a5 5 0 100 10 5 5 0 000-10z" stroke="#primary" stroke-width="2" stroke-linecap="round"/><path d="M37 51c-.92-4.01-4.6-7-9-7-4.401 0-8.083 2.995-9 7" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M28.01 31.004a6.5 6.5 0 110 13 6.5 6.5 0 010-13z" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M28 14.011a5 5 0 11-5 4.998 5 5 0 015-4.998z" stroke="#primary" stroke-width="2" stroke-linecap="round"/><path d="M22 26c1.558-1.25 3.665-2 6-2 2.319 0 4.439.761 6 2" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M51 9V8c0-1.3-1.574-3-3-3h-8c-1.426 0-3 1.7-3 3v13l4-4h6c2.805-.031 4-1.826 4-4V9zM5 9V8c0-1.3 1.574-3 3-3h8c1.426 0 3 1.7 3 3v13l-4-4H9c-2.805-.031-4-1.826-4-4V9z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M43 11a1 1 0 11-2 0 1 1 0 012 0zm4 0a1 1 0 11-2 0 1 1 0 012 0zm-36 0a1 1 0 11-2 0 1 1 0 012 0zm4 0a1 1 0 11-2 0 1 1 0 012 0z" fill="#secondary"/></g>',
...rank(value, [1, 100, 500, 1000, 2500]), ...rank(value, [1, 100, 500, 1000, 2500]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
//Member //Member
{ {
const {years:value} = computed.registered const {years: value} = computed.registered
const unlock = null const unlock = null
list.push({ list.push({
title:"Member", title: "Member",
text:`Registered ${Math.floor(value)} year${imports.s(Math.floor(value))} ago`, text: `Registered ${Math.floor(value)} year${imports.s(Math.floor(value))} ago`,
icon:'<g xmlns="http://www.w3.org/2000/svg" transform="translate(5 4)" fill="none" fill-rule="evenodd"><path d="M46 44.557v1a2 2 0 01-2 2H2a2 2 0 01-2-2v-1" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M.75 40.993l.701.561a2.323 2.323 0 002.903 0l1.675-1.34a3 3 0 013.748 0l1.282 1.026a3 3 0 003.71.03l1.4-1.085a3 3 0 013.75.061l1.103.913a3 3 0 003.787.031l1.22-.976a3 3 0 013.748 0l1.282 1.026a3 3 0 003.71.03l1.4-1.085a3 3 0 013.75.061l1.429 1.182a2.427 2.427 0 003.103-.008l.832-.695A2 2 0 0046 39.191v-1.634a2 2 0 00-2-2H2a2 2 0 00-2 2v1.875a2 2 0 00.75 1.561z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M42 31.609v.948m-38 0v-.992m25.04-15.008H35a2 2 0 012 2v1m-28 0v-1a2 2 0 012-2h6.007" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 8.557h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6a1 1 0 011-1z" stroke="#primary" stroke-width="2" stroke-linejoin="round"/><path d="M4.7 10.557c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689zm35-8c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M23 5.557a2 2 0 002-2C25 2.452 24.433 0 22.273 0c-.463 0 .21 1.424-.502 1.979A2 2 0 0023 5.557z" stroke="#primary" stroke-width="2"/><path d="M4.78 27.982l1.346 1.076a3 3 0 003.748 0l1.252-1.002a3 3 0 013.748 0l1.282 1.026a3 3 0 003.711.03l1.4-1.085a3 3 0 013.75.061l1.102.913a3 3 0 003.787.031l1.22-.976a3 3 0 013.748 0l1.281 1.025a3 3 0 003.712.029l1.358-1.053a2 2 0 00.775-1.58v-.97a1.95 1.95 0 00-1.95-1.95H5.942a1.912 1.912 0 00-1.912 1.912v.951a2 2 0 00.75 1.562z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle stroke="#secondary" cx="16.5" cy="2.057" r="1"/><circle stroke="#secondary" cx="14.5" cy="12.057" r="1"/><circle stroke="#secondary" cx="31.5" cy="9.057" r="1"/></g>', icon:
'<g xmlns="http://www.w3.org/2000/svg" transform="translate(5 4)" fill="none" fill-rule="evenodd"><path d="M46 44.557v1a2 2 0 01-2 2H2a2 2 0 01-2-2v-1" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M.75 40.993l.701.561a2.323 2.323 0 002.903 0l1.675-1.34a3 3 0 013.748 0l1.282 1.026a3 3 0 003.71.03l1.4-1.085a3 3 0 013.75.061l1.103.913a3 3 0 003.787.031l1.22-.976a3 3 0 013.748 0l1.282 1.026a3 3 0 003.71.03l1.4-1.085a3 3 0 013.75.061l1.429 1.182a2.427 2.427 0 003.103-.008l.832-.695A2 2 0 0046 39.191v-1.634a2 2 0 00-2-2H2a2 2 0 00-2 2v1.875a2 2 0 00.75 1.561z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M42 31.609v.948m-38 0v-.992m25.04-15.008H35a2 2 0 012 2v1m-28 0v-1a2 2 0 012-2h6.007" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 8.557h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6a1 1 0 011-1z" stroke="#primary" stroke-width="2" stroke-linejoin="round"/><path d="M4.7 10.557c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689zm35-8c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M23 5.557a2 2 0 002-2C25 2.452 24.433 0 22.273 0c-.463 0 .21 1.424-.502 1.979A2 2 0 0023 5.557z" stroke="#primary" stroke-width="2"/><path d="M4.78 27.982l1.346 1.076a3 3 0 003.748 0l1.252-1.002a3 3 0 013.748 0l1.282 1.026a3 3 0 003.711.03l1.4-1.085a3 3 0 013.75.061l1.102.913a3 3 0 003.787.031l1.22-.976a3 3 0 013.748 0l1.281 1.025a3 3 0 003.712.029l1.358-1.053a2 2 0 00.775-1.58v-.97a1.95 1.95 0 00-1.95-1.95H5.942a1.912 1.912 0 00-1.912 1.912v.951a2 2 0 00.75 1.562z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle stroke="#secondary" cx="16.5" cy="2.057" r="1"/><circle stroke="#secondary" cx="14.5" cy="12.057" r="1"/><circle stroke="#secondary" cx="31.5" cy="9.057" r="1"/></g>',
...rank(value, [1, 3, 5, 10, 15]), ...rank(value, [1, 3, 5, 10, 15]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
} }

View File

@@ -2,22 +2,23 @@
export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) { export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) {
//Initialization //Initialization
const {user} = await graphql(queries.achievements({login})) const {user} = await graphql(queries.achievements({login}))
const scores = {followers:user.followers.totalCount, created:user.repositories.totalCount, stars:user.popular.nodes?.[0]?.stargazers?.totalCount ?? 0, forks:Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount))} const scores = {followers: user.followers.totalCount, created: user.repositories.totalCount, stars: user.popular.nodes?.[0]?.stargazers?.totalCount ?? 0, forks: Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount))}
const ranks = await graphql(queries.achievements.ranking(scores)) const ranks = await graphql(queries.achievements.ranking(scores))
const requirements = {stars:5, followers:3, forks:1, created:1} const requirements = {stars: 5, followers: 3, forks: 1, created: 1}
//Developer //Developer
{ {
const value = user.repositories.totalCount const value = user.repositories.totalCount
const unlock = user.repositories.nodes?.shift() const unlock = user.repositories.nodes?.shift()
list.push({ list.push({
title:"Developer", title: "Developer",
text:`Published ${value} public repositor${imports.s(value, "y")}`, text: `Published ${value} public repositor${imports.s(value, "y")}`,
icon:'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#primary"><path d="M20 24l-3.397 3.398a.85.85 0 000 1.203L20.002 32M37.015 24l3.399 3.398a.85.85 0 010 1.203L37.014 32" stroke-linejoin="round"/><path d="M31.029 21.044L25.976 35.06"/></g><path stroke="#secondary" stroke-linejoin="round" d="M23.018 10h8.984M26 47h5M8 16h16m9 0h15.725M8 41h13"/><path d="M5.027 34.998c.673 2.157 1.726 4.396 2.81 6.02m43.38-19.095C50.7 19.921 49.866 17.796 48.79 16" stroke="#secondary"/><path stroke="#primary" stroke-linejoin="round" d="M26 41h17"/><path d="M7.183 16C5.186 19.582 4 23.619 4 28M42.608 47.02c2.647-1.87 5.642-5.448 7.295-9.18C51.52 34.191 52.071 30.323 52 28" stroke="#primary"/><path stroke="#primary" stroke-linejoin="round" d="M7.226 16H28M13.343 47H21"/><path d="M13.337 47.01a24.364 24.364 0 006.19 3.45 24.527 24.527 0 007.217 1.505c2.145.108 4.672-.05 7.295-.738" stroke="#primary"/><path stroke="#primary" stroke-linejoin="round" d="M36 47h6.647M12 10h6M37 10h6.858"/><path d="M43.852 10c-4.003-3.667-9.984-6.054-16.047-6-2.367.021-4.658.347-6.81 1.045" stroke="#primary"/><path stroke="#secondary" stroke-linejoin="round" d="M5.041 35h4.962M47 22h4.191"/></g>', icon:
'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#primary"><path d="M20 24l-3.397 3.398a.85.85 0 000 1.203L20.002 32M37.015 24l3.399 3.398a.85.85 0 010 1.203L37.014 32" stroke-linejoin="round"/><path d="M31.029 21.044L25.976 35.06"/></g><path stroke="#secondary" stroke-linejoin="round" d="M23.018 10h8.984M26 47h5M8 16h16m9 0h15.725M8 41h13"/><path d="M5.027 34.998c.673 2.157 1.726 4.396 2.81 6.02m43.38-19.095C50.7 19.921 49.866 17.796 48.79 16" stroke="#secondary"/><path stroke="#primary" stroke-linejoin="round" d="M26 41h17"/><path d="M7.183 16C5.186 19.582 4 23.619 4 28M42.608 47.02c2.647-1.87 5.642-5.448 7.295-9.18C51.52 34.191 52.071 30.323 52 28" stroke="#primary"/><path stroke="#primary" stroke-linejoin="round" d="M7.226 16H28M13.343 47H21"/><path d="M13.337 47.01a24.364 24.364 0 006.19 3.45 24.527 24.527 0 007.217 1.505c2.145.108 4.672-.05 7.295-.738" stroke="#primary"/><path stroke="#primary" stroke-linejoin="round" d="M36 47h6.647M12 10h6M37 10h6.858"/><path d="M43.852 10c-4.003-3.667-9.984-6.054-16.047-6-2.367.021-4.658.347-6.81 1.045" stroke="#primary"/><path stroke="#secondary" stroke-linejoin="round" d="M5.041 35h4.962M47 22h4.191"/></g>',
...rank(value, [1, 20, 50, 100, 250]), ...rank(value, [1, 20, 50, 100, 250]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
leaderboard:leaderboard({user:ranks.created_rank.userCount, requirement:scores.created >= requirements.created, type:"users"}), leaderboard: leaderboard({user: ranks.created_rank.userCount, requirement: scores.created >= requirements.created, type: "users"}),
}) })
} }
@@ -26,12 +27,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const value = user.forks.totalCount const value = user.forks.totalCount
const unlock = user.forks.nodes?.shift() const unlock = user.forks.nodes?.shift()
list.push({ list.push({
title:"Forker", title: "Forker",
text:`Forked ${value} public repositor${imports.s(value, "y")}`, text: `Forked ${value} public repositor${imports.s(value, "y")}`,
icon:'<g stroke-width="2" fill="none" fill-rule="evenodd"><path d="M37.303 21.591a5.84 5.84 0 00-1.877-1.177 6.138 6.138 0 00-4.432 0 5.822 5.822 0 00-1.879 1.177L28 22.638l-1.115-1.047c-1.086-1.018-2.559-1.59-4.094-1.59-1.536 0-3.008.572-4.094 1.59-1.086 1.02-1.696 2.4-1.696 3.84 0 1.441.61 2.823 1.696 3.841l1.115 1.046L28 38l8.189-7.682 1.115-1.046a5.422 5.422 0 001.256-1.761 5.126 5.126 0 000-4.157 5.426 5.426 0 00-1.256-1.763z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.967 42.705A18.922 18.922 0 0028 47a18.92 18.92 0 0011.076-3.56m-.032-30.902A18.914 18.914 0 0028 9c-4.09 0-7.876 1.292-10.976 3.49" stroke="#secondary" stroke-linecap="round"/><g transform="translate(7 10)" stroke="#primary"><path d="M6 0v7c0 2.21-1.343 3-3 3s-3-.79-3-3V0" stroke-linecap="round" stroke-linejoin="round"/><path stroke-linecap="round" d="M3 0v19.675"/><rect stroke-linejoin="round" x="1" y="20" width="4" height="16" rx="2"/></g><g transform="translate(43 10)" stroke="#primary"><path stroke-linecap="round" d="M2 15.968v3.674"/><path d="M4 15.642H0L.014 4.045A4.05 4.05 0 014.028 0L4 15.642z" stroke-linecap="round" stroke-linejoin="round"/><rect stroke-linejoin="round" y="19.968" width="4" height="16" rx="2"/></g><path d="M41.364 8.062A23.888 23.888 0 0028 4a23.89 23.89 0 00-11.95 3.182M4.75 22.021A24.045 24.045 0 004 28c0 1.723.182 3.404.527 5.024m10.195 14.971A23.888 23.888 0 0028 52c4.893 0 9.444-1.464 13.239-3.979m9-10.98A23.932 23.932 0 0052 28c0-2.792-.477-5.472-1.353-7.964" stroke="#secondary" stroke-linecap="round"/></g>', icon:
'<g stroke-width="2" fill="none" fill-rule="evenodd"><path d="M37.303 21.591a5.84 5.84 0 00-1.877-1.177 6.138 6.138 0 00-4.432 0 5.822 5.822 0 00-1.879 1.177L28 22.638l-1.115-1.047c-1.086-1.018-2.559-1.59-4.094-1.59-1.536 0-3.008.572-4.094 1.59-1.086 1.02-1.696 2.4-1.696 3.84 0 1.441.61 2.823 1.696 3.841l1.115 1.046L28 38l8.189-7.682 1.115-1.046a5.422 5.422 0 001.256-1.761 5.126 5.126 0 000-4.157 5.426 5.426 0 00-1.256-1.763z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path d="M15.967 42.705A18.922 18.922 0 0028 47a18.92 18.92 0 0011.076-3.56m-.032-30.902A18.914 18.914 0 0028 9c-4.09 0-7.876 1.292-10.976 3.49" stroke="#secondary" stroke-linecap="round"/><g transform="translate(7 10)" stroke="#primary"><path d="M6 0v7c0 2.21-1.343 3-3 3s-3-.79-3-3V0" stroke-linecap="round" stroke-linejoin="round"/><path stroke-linecap="round" d="M3 0v19.675"/><rect stroke-linejoin="round" x="1" y="20" width="4" height="16" rx="2"/></g><g transform="translate(43 10)" stroke="#primary"><path stroke-linecap="round" d="M2 15.968v3.674"/><path d="M4 15.642H0L.014 4.045A4.05 4.05 0 014.028 0L4 15.642z" stroke-linecap="round" stroke-linejoin="round"/><rect stroke-linejoin="round" y="19.968" width="4" height="16" rx="2"/></g><path d="M41.364 8.062A23.888 23.888 0 0028 4a23.89 23.89 0 00-11.95 3.182M4.75 22.021A24.045 24.045 0 004 28c0 1.723.182 3.404.527 5.024m10.195 14.971A23.888 23.888 0 0028 52c4.893 0 9.444-1.464 13.239-3.979m9-10.98A23.932 23.932 0 0052 28c0-2.792-.477-5.472-1.353-7.964" stroke="#secondary" stroke-linecap="round"/></g>',
...rank(value, [1, 5, 10, 20, 50]), ...rank(value, [1, 5, 10, 20, 50]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -41,12 +43,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = user.pullRequests.nodes?.shift() const unlock = user.pullRequests.nodes?.shift()
list.push({ list.push({
title:"Contributor", title: "Contributor",
text:`Opened ${value} pull request${imports.s(value)}`, text: `Opened ${value} pull request${imports.s(value)}`,
icon:'<g stroke-width="2" fill="none" fill-rule="evenodd"><path stroke="#secondary" stroke-linecap="round" stroke-linejoin="round" d="M26.022 5.014h6M26.012 53.005h6M27.003 47.003h12M44.01 20.005h5M19.01 11.003h12"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M38.005 11.008h6M41 14.013v-6M14.007 47.003h6M17.002 50.004v-6"/><path d="M29.015 5.01l-5.003 5.992 5.003-5.992zM33.015 47.01l-5.003 5.992 5.003-5.992z" stroke="#secondary"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M8.01 19.002h6"/><path stroke="#secondary" stroke-linecap="round" stroke-linejoin="round" d="M47.011 29h6"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M44.012 39.003h6"/><g stroke="#secondary"><path d="M5.36 29c4.353 0 6.4 4.472 6.4 8"/><path stroke-linecap="round" stroke-linejoin="round" d="M13.99 37.995h-5M10.989 29h-6"/></g><path d="M24.503 22c1.109 0 2.007.895 2.007 2 0 1.104-.898 2-2.007 2a2.004 2.004 0 01-2.008-2c0-1.105.9-2 2.008-2zM24.5 32a2 2 0 110 4 2 2 0 010-4zm9.5 0a2 2 0 110 4 2 2 0 010-4z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" d="M24.5 26.004v6.001"/><path d="M31.076 23.988l1.027-.023a1.998 1.998 0 011.932 2.01L34 31" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M31.588 26.222l-2.194-2.046 2.046-2.194"/><path d="M29.023 43c7.732 0 14-6.268 14-14s-6.268-14-14-14-14 6.268-14 14 6.268 14 14 14z" stroke="#primary"/></g>', icon:
'<g stroke-width="2" fill="none" fill-rule="evenodd"><path stroke="#secondary" stroke-linecap="round" stroke-linejoin="round" d="M26.022 5.014h6M26.012 53.005h6M27.003 47.003h12M44.01 20.005h5M19.01 11.003h12"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M38.005 11.008h6M41 14.013v-6M14.007 47.003h6M17.002 50.004v-6"/><path d="M29.015 5.01l-5.003 5.992 5.003-5.992zM33.015 47.01l-5.003 5.992 5.003-5.992z" stroke="#secondary"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M8.01 19.002h6"/><path stroke="#secondary" stroke-linecap="round" stroke-linejoin="round" d="M47.011 29h6"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M44.012 39.003h6"/><g stroke="#secondary"><path d="M5.36 29c4.353 0 6.4 4.472 6.4 8"/><path stroke-linecap="round" stroke-linejoin="round" d="M13.99 37.995h-5M10.989 29h-6"/></g><path d="M24.503 22c1.109 0 2.007.895 2.007 2 0 1.104-.898 2-2.007 2a2.004 2.004 0 01-2.008-2c0-1.105.9-2 2.008-2zM24.5 32a2 2 0 110 4 2 2 0 010-4zm9.5 0a2 2 0 110 4 2 2 0 010-4z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" d="M24.5 26.004v6.001"/><path d="M31.076 23.988l1.027-.023a1.998 1.998 0 011.932 2.01L34 31" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M31.588 26.222l-2.194-2.046 2.046-2.194"/><path d="M29.023 43c7.732 0 14-6.268 14-14s-6.268-14-14-14-14 6.268-14 14 6.268 14 14 14z" stroke="#primary"/></g>',
...rank(value, [1, 200, 500, 1000, 2500]), ...rank(value, [1, 200, 500, 1000, 2500]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -56,12 +59,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = user.projects.nodes?.shift() const unlock = user.projects.nodes?.shift()
list.push({ list.push({
title:"Manager", title: "Manager",
text:`Created ${value} user project${imports.s(value)}`, text: `Created ${value} user project${imports.s(value)}`,
icon:'<g stroke-width="2" fill="none" fill-rule="evenodd"><path d="M29 16V8.867C29 7.705 29.627 7 30.692 7h18.616C50.373 7 51 7.705 51 8.867v38.266C51 48.295 50.373 49 49.308 49H30.692C29.627 49 29 48.295 29 47.133V39m-4-23V9c0-1.253-.737-2-2-2H7c-1.263 0-2 .747-2 2v34c0 1.253.737 2 2 2h16c1.263 0 2-.747 2-2v-4" stroke="#secondary" stroke-linecap="round"/><path stroke="#secondary" d="M51.557 12.005h-22M5 12.005h21"/><path d="M14 33V22c0-1.246.649-2 1.73-2h28.54c1.081 0 1.73.754 1.73 2v11c0 1.246-.649 2-1.73 2H15.73c-1.081 0-1.73-.754-1.73-2z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path d="M19 29v-3c0-.508.492-1 1-1h3c.508 0 1 .492 1 1v3c0 .508-.492 1-1 1h-3c-.508-.082-1-.492-1-1z" stroke="#primary"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M28.996 27.998h12M9.065 20.04a7.062 7.062 0 00-.023 1.728m.775 2.517c.264.495.584.954.954 1.369"/></g>', icon:
'<g stroke-width="2" fill="none" fill-rule="evenodd"><path d="M29 16V8.867C29 7.705 29.627 7 30.692 7h18.616C50.373 7 51 7.705 51 8.867v38.266C51 48.295 50.373 49 49.308 49H30.692C29.627 49 29 48.295 29 47.133V39m-4-23V9c0-1.253-.737-2-2-2H7c-1.263 0-2 .747-2 2v34c0 1.253.737 2 2 2h16c1.263 0 2-.747 2-2v-4" stroke="#secondary" stroke-linecap="round"/><path stroke="#secondary" d="M51.557 12.005h-22M5 12.005h21"/><path d="M14 33V22c0-1.246.649-2 1.73-2h28.54c1.081 0 1.73.754 1.73 2v11c0 1.246-.649 2-1.73 2H15.73c-1.081 0-1.73-.754-1.73-2z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path d="M19 29v-3c0-.508.492-1 1-1h3c.508 0 1 .492 1 1v3c0 .508-.492 1-1 1h-3c-.508-.082-1-.492-1-1z" stroke="#primary"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M28.996 27.998h12M9.065 20.04a7.062 7.062 0 00-.023 1.728m.775 2.517c.264.495.584.954.954 1.369"/></g>',
...rank(value, [1, 2, 3, 4, 5]), ...rank(value, [1, 2, 3, 4, 5]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -71,12 +75,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = user.contributionsCollection.pullRequestReviewContributions.nodes?.shift() const unlock = user.contributionsCollection.pullRequestReviewContributions.nodes?.shift()
list.push({ list.push({
title:"Reviewer", title: "Reviewer",
text:`Reviewed ${value} pull request${imports.s(value)}`, text: `Reviewed ${value} pull request${imports.s(value)}`,
icon:'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#secondary"><path d="M26.009 34.01c.444-.004.9.141 1.228.414.473.394.766.959.76 1.54-.01.735-.333 1.413-.97 2.037.66.718.985 1.4.976 2.048-.012.828-.574 1.58-1.687 2.258.624.788.822 1.549.596 2.28-.225.733-.789 1.219-1.69 1.459.703.833.976 1.585.82 2.256-.178.763-.313 1.716-2.492 1.711" stroke-linejoin="round"/><g stroke-linejoin="round"><path d="M18.548 28.422c1.184-4.303-2.132-5.292-2.132-5.292-.873 2.296-1.438 3.825-4.231 8.108-1.285 1.97-1.926 3.957-1.877 5.796M18.391 34.011L24.993 34c2.412-.009.211-.005-6.602.012zM5.004 37.017l5.234-.014-5.234.014z"/></g><g stroke-linejoin="round"><path d="M18.548 28.422c1.184-4.303-2.132-5.292-2.132-5.292-.873 2.296-1.438 3.825-4.231 8.108-1.285 1.97-1.926 3.957-1.877 5.796M5.004 37.017l5.234-.014-5.234.014zM7 48.012h4.01c1.352 1.333 2.672 2 3.961 2.001 0 0 .485-.005 5.46-.005h3.536"/></g><path d="M18.793 27.022c-.062.933-.373 2.082-.933 3.446-.561 1.364-.433 2.547.383 3.547"/></g><path d="M45 16.156V23a2 2 0 01-2 2H31l-6 4v-4h-1.934M12 23V8a2 2 0 012-2h29a2 2 0 012 2v10" stroke="#primary" stroke-linejoin="round"/><path stroke="#primary" stroke-linejoin="round" d="M23 12.014l-3 3 3 3M34 12.014l3 3-3 3"/><path stroke="#primary" d="M30.029 10l-3.015 10.027"/><path d="M32 39h3l6 4v-4h8a2 2 0 002-2V22a2 2 0 00-2-2h.138" stroke="#secondary" stroke-linejoin="round"/><path stroke="#primary" stroke-linejoin="round" d="M33 29h12M33 34h6M43 34h2"/></g>', icon:
'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#secondary"><path d="M26.009 34.01c.444-.004.9.141 1.228.414.473.394.766.959.76 1.54-.01.735-.333 1.413-.97 2.037.66.718.985 1.4.976 2.048-.012.828-.574 1.58-1.687 2.258.624.788.822 1.549.596 2.28-.225.733-.789 1.219-1.69 1.459.703.833.976 1.585.82 2.256-.178.763-.313 1.716-2.492 1.711" stroke-linejoin="round"/><g stroke-linejoin="round"><path d="M18.548 28.422c1.184-4.303-2.132-5.292-2.132-5.292-.873 2.296-1.438 3.825-4.231 8.108-1.285 1.97-1.926 3.957-1.877 5.796M18.391 34.011L24.993 34c2.412-.009.211-.005-6.602.012zM5.004 37.017l5.234-.014-5.234.014z"/></g><g stroke-linejoin="round"><path d="M18.548 28.422c1.184-4.303-2.132-5.292-2.132-5.292-.873 2.296-1.438 3.825-4.231 8.108-1.285 1.97-1.926 3.957-1.877 5.796M5.004 37.017l5.234-.014-5.234.014zM7 48.012h4.01c1.352 1.333 2.672 2 3.961 2.001 0 0 .485-.005 5.46-.005h3.536"/></g><path d="M18.793 27.022c-.062.933-.373 2.082-.933 3.446-.561 1.364-.433 2.547.383 3.547"/></g><path d="M45 16.156V23a2 2 0 01-2 2H31l-6 4v-4h-1.934M12 23V8a2 2 0 012-2h29a2 2 0 012 2v10" stroke="#primary" stroke-linejoin="round"/><path stroke="#primary" stroke-linejoin="round" d="M23 12.014l-3 3 3 3M34 12.014l3 3-3 3"/><path stroke="#primary" d="M30.029 10l-3.015 10.027"/><path d="M32 39h3l6 4v-4h8a2 2 0 002-2V22a2 2 0 00-2-2h.138" stroke="#secondary" stroke-linejoin="round"/><path stroke="#primary" stroke-linejoin="round" d="M33 29h12M33 34h6M43 34h2"/></g>',
...rank(value, [1, 200, 500, 1000, 2500]), ...rank(value, [1, 200, 500, 1000, 2500]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -86,12 +91,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = user.packages.nodes?.shift() const unlock = user.packages.nodes?.shift()
list.push({ list.push({
title:"Packager", title: "Packager",
text:`Created ${value} package${imports.s(value)}`, text: `Created ${value} package${imports.s(value)}`,
icon:'<g fill="none"><path fill="#secondary" d="M28.53 27.64l-11.2 6.49V21.15l11.23-6.48z"/><path d="M40.4 34.84c-.17 0-.34-.04-.5-.13l-11.24-6.44a.99.99 0 01-.37-1.36.99.99 0 011.36-.37l11.24 6.44c.48.27.65.89.37 1.36-.17.32-.51.5-.86.5z" fill="#primary"/><path d="M29.16 28.4c-.56 0-1-.45-1-1.01l.08-12.47c0-.55.49-1 1.01-.99.55 0 1 .45.99 1.01l-.08 12.47c0 .55-.45.99-1 .99z" fill="#primary"/><path d="M18.25 34.65a.996.996 0 01-.5-1.86l10.91-6.25a.997.997 0 11.99 1.73l-10.91 6.25c-.15.09-.32.13-.49.13z" fill="#primary"/><path d="M29.19 41.37c-.17 0-.35-.04-.5-.13l-11.23-6.49c-.31-.18-.5-.51-.5-.87V20.91c0-.36.19-.69.5-.87l11.23-6.49c.31-.18.69-.18 1 0l11.23 6.49c.31.18.5.51.5.87v12.97c0 .36-.19.69-.5.87l-11.23 6.49c-.15.08-.32.13-.5.13zm-10.23-8.06l10.23 5.91 10.23-5.91V21.49l-10.23-5.91-10.23 5.91v11.82zM40.5 11.02c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm-23.19 4.36c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.42 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm23.37 43.8c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.42 3.18-3.18 3.18zm0-4.35c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm-23.06 4.11c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zM6.18 30.72C4.43 30.72 3 29.29 3 27.54c0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm45.64 4.36c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18z" fill="#primary"/><path d="M29.1 10.21c-.55 0-1-.45-1-1V3.52c0-.55.45-1 1-1s1 .45 1 1v5.69c0 .56-.45 1-1 1zM7.44 20.95c-.73 0-1.32-.59-1.32-1.32v-5.38l4.66-2.69c.63-.37 1.44-.15 1.8.48.36.63.15 1.44-.48 1.8l-3.34 1.93v3.86c0 .73-.59 1.32-1.32 1.32zm4 22.68c-.22 0-.45-.06-.66-.18l-4.66-2.69v-5.38c0-.73.59-1.32 1.32-1.32.73 0 1.32.59 1.32 1.32v3.86l3.34 1.93c.63.36.85 1.17.48 1.8-.24.42-.68.66-1.14.66zm17.64 10.39l-4.66-2.69c-.63-.36-.85-1.17-.48-1.8.36-.63 1.17-.85 1.8-.48l3.34 1.93 3.34-1.93a1.32 1.32 0 011.8.48c.36.63.15 1.44-.48 1.8l-4.66 2.69zm17.64-10.39a1.32 1.32 0 01-.66-2.46l3.34-1.93v-3.86c0-.73.59-1.32 1.32-1.32.73 0 1.32.59 1.32 1.32v5.38l-4.66 2.69c-.21.12-.44.18-.66.18zm4-22.68c-.73 0-1.32-.59-1.32-1.32v-3.86l-3.34-1.93c-.63-.36-.85-1.17-.48-1.8.36-.63 1.17-.85 1.8-.48l4.66 2.69v5.38c0 .73-.59 1.32-1.32 1.32z" fill="#secondary"/><path d="M33.08 6.15c-.22 0-.45-.06-.66-.18l-3.34-1.93-3.34 1.93c-.63.36-1.44.15-1.8-.48a1.32 1.32 0 01.48-1.8L29.08 1l4.66 2.69c.63.36.85 1.17.48 1.8a1.3 1.3 0 01-1.14.66zm-3.99 47.3c-.55 0-1-.45-1-1v-7.13c0-.55.45-1 1-1s1 .45 1 1v7.13c0 .55-.44 1-1 1zM13.86 19.71c-.17 0-.34-.04-.5-.13L7.2 16a1 1 0 011-1.73l6.17 3.58c.48.28.64.89.36 1.37-.19.31-.52.49-.87.49zm36.63 21.23c-.17 0-.34-.04-.5-.13l-6.17-3.57a.998.998 0 01-.36-1.37c.28-.48.89-.64 1.37-.36L51 39.08c.48.28.64.89.36 1.37-.19.31-.52.49-.87.49zM44.06 19.8c-.35 0-.68-.18-.87-.5-.28-.48-.11-1.09.36-1.37l6.17-3.57c.48-.28 1.09-.11 1.37.36.28.48.11 1.09-.36 1.37l-6.17 3.57c-.16.1-.33.14-.5.14zM7.43 41.03c-.35 0-.68-.18-.87-.5-.28-.48-.11-1.09.36-1.37l6.17-3.57c.48-.28 1.09-.11 1.37.36.28.48.11 1.09-.36 1.37l-6.17 3.57c-.15.09-.33.14-.5.14z" fill="#secondary"/></g>', icon:
'<g fill="none"><path fill="#secondary" d="M28.53 27.64l-11.2 6.49V21.15l11.23-6.48z"/><path d="M40.4 34.84c-.17 0-.34-.04-.5-.13l-11.24-6.44a.99.99 0 01-.37-1.36.99.99 0 011.36-.37l11.24 6.44c.48.27.65.89.37 1.36-.17.32-.51.5-.86.5z" fill="#primary"/><path d="M29.16 28.4c-.56 0-1-.45-1-1.01l.08-12.47c0-.55.49-1 1.01-.99.55 0 1 .45.99 1.01l-.08 12.47c0 .55-.45.99-1 .99z" fill="#primary"/><path d="M18.25 34.65a.996.996 0 01-.5-1.86l10.91-6.25a.997.997 0 11.99 1.73l-10.91 6.25c-.15.09-.32.13-.49.13z" fill="#primary"/><path d="M29.19 41.37c-.17 0-.35-.04-.5-.13l-11.23-6.49c-.31-.18-.5-.51-.5-.87V20.91c0-.36.19-.69.5-.87l11.23-6.49c.31-.18.69-.18 1 0l11.23 6.49c.31.18.5.51.5.87v12.97c0 .36-.19.69-.5.87l-11.23 6.49c-.15.08-.32.13-.5.13zm-10.23-8.06l10.23 5.91 10.23-5.91V21.49l-10.23-5.91-10.23 5.91v11.82zM40.5 11.02c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm-23.19 4.36c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.42 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm23.37 43.8c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.42 3.18-3.18 3.18zm0-4.35c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm-23.06 4.11c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zM6.18 30.72C4.43 30.72 3 29.29 3 27.54c0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18zm45.64 4.36c-1.75 0-3.18-1.43-3.18-3.18 0-1.75 1.43-3.18 3.18-3.18 1.75 0 3.18 1.43 3.18 3.18 0 1.75-1.43 3.18-3.18 3.18zm0-4.36c-.65 0-1.18.53-1.18 1.18 0 .65.53 1.18 1.18 1.18.65 0 1.18-.53 1.18-1.18 0-.65-.53-1.18-1.18-1.18z" fill="#primary"/><path d="M29.1 10.21c-.55 0-1-.45-1-1V3.52c0-.55.45-1 1-1s1 .45 1 1v5.69c0 .56-.45 1-1 1zM7.44 20.95c-.73 0-1.32-.59-1.32-1.32v-5.38l4.66-2.69c.63-.37 1.44-.15 1.8.48.36.63.15 1.44-.48 1.8l-3.34 1.93v3.86c0 .73-.59 1.32-1.32 1.32zm4 22.68c-.22 0-.45-.06-.66-.18l-4.66-2.69v-5.38c0-.73.59-1.32 1.32-1.32.73 0 1.32.59 1.32 1.32v3.86l3.34 1.93c.63.36.85 1.17.48 1.8-.24.42-.68.66-1.14.66zm17.64 10.39l-4.66-2.69c-.63-.36-.85-1.17-.48-1.8.36-.63 1.17-.85 1.8-.48l3.34 1.93 3.34-1.93a1.32 1.32 0 011.8.48c.36.63.15 1.44-.48 1.8l-4.66 2.69zm17.64-10.39a1.32 1.32 0 01-.66-2.46l3.34-1.93v-3.86c0-.73.59-1.32 1.32-1.32.73 0 1.32.59 1.32 1.32v5.38l-4.66 2.69c-.21.12-.44.18-.66.18zm4-22.68c-.73 0-1.32-.59-1.32-1.32v-3.86l-3.34-1.93c-.63-.36-.85-1.17-.48-1.8.36-.63 1.17-.85 1.8-.48l4.66 2.69v5.38c0 .73-.59 1.32-1.32 1.32z" fill="#secondary"/><path d="M33.08 6.15c-.22 0-.45-.06-.66-.18l-3.34-1.93-3.34 1.93c-.63.36-1.44.15-1.8-.48a1.32 1.32 0 01.48-1.8L29.08 1l4.66 2.69c.63.36.85 1.17.48 1.8a1.3 1.3 0 01-1.14.66zm-3.99 47.3c-.55 0-1-.45-1-1v-7.13c0-.55.45-1 1-1s1 .45 1 1v7.13c0 .55-.44 1-1 1zM13.86 19.71c-.17 0-.34-.04-.5-.13L7.2 16a1 1 0 011-1.73l6.17 3.58c.48.28.64.89.36 1.37-.19.31-.52.49-.87.49zm36.63 21.23c-.17 0-.34-.04-.5-.13l-6.17-3.57a.998.998 0 01-.36-1.37c.28-.48.89-.64 1.37-.36L51 39.08c.48.28.64.89.36 1.37-.19.31-.52.49-.87.49zM44.06 19.8c-.35 0-.68-.18-.87-.5-.28-.48-.11-1.09.36-1.37l6.17-3.57c.48-.28 1.09-.11 1.37.36.28.48.11 1.09-.36 1.37l-6.17 3.57c-.16.1-.33.14-.5.14zM7.43 41.03c-.35 0-.68-.18-.87-.5-.28-.48-.11-1.09.36-1.37l6.17-3.57c.48-.28 1.09-.11 1.37.36.28.48.11 1.09-.36 1.37l-6.17 3.57c-.15.09-.33.14-.5.14z" fill="#secondary"/></g>',
...rank(value, [1, 5, 10, 20, 30]), ...rank(value, [1, 5, 10, 20, 30]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -101,12 +107,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = user.gists.nodes?.shift() const unlock = user.gists.nodes?.shift()
list.push({ list.push({
title:"Gister", title: "Gister",
text:`Published ${value} gist${imports.s(value)}`, text: `Published ${value} gist${imports.s(value)}`,
icon:'<g stroke-width="2" fill="none" fill-rule="evenodd"><path d="M20 48.875v-12.75c0-1.33.773-2.131 2.385-2.125h26.23c1.612-.006 2.385.795 2.385 2.125v12.75C51 50.198 50.227 51 48.615 51h-26.23C20.773 51 20 50.198 20 48.875zM37 40.505h9M37 44.492h6" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#secondary" stroke-linecap="round" stroke-linejoin="round" d="M14 30h-4M16 35h-3M47 10H5M42 15H24M19 15h-9M16 25h-3M42 20h-2M42 20h-2M42 25h-2M16 20h-3"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M31.974 25H24"/><path d="M22 20h12a2 2 0 012 2v6a2 2 0 01-2 2H22a2 2 0 01-2-2v-6a2 2 0 012-2z" stroke="#primary"/><path d="M5 33V7a2 2 0 012-2h38a2 2 0 012 2v23" stroke="#secondary" stroke-linecap="round"/><path d="M5 30v8c0 1.105.892 2 1.993 2H16" stroke="#secondary" stroke-linecap="round"/><g stroke="#primary" stroke-linecap="round"><path d="M26.432 37.933v7.07M26.432 37.933v9.07M24.432 40.433h7.07M24.432 40.433h8.07M24.432 44.433h7.07M24.432 44.433h8.07M30.432 37.933v9.07"/></g></g>', icon:
'<g stroke-width="2" fill="none" fill-rule="evenodd"><path d="M20 48.875v-12.75c0-1.33.773-2.131 2.385-2.125h26.23c1.612-.006 2.385.795 2.385 2.125v12.75C51 50.198 50.227 51 48.615 51h-26.23C20.773 51 20 50.198 20 48.875zM37 40.505h9M37 44.492h6" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#secondary" stroke-linecap="round" stroke-linejoin="round" d="M14 30h-4M16 35h-3M47 10H5M42 15H24M19 15h-9M16 25h-3M42 20h-2M42 20h-2M42 25h-2M16 20h-3"/><path stroke="#primary" stroke-linecap="round" stroke-linejoin="round" d="M31.974 25H24"/><path d="M22 20h12a2 2 0 012 2v6a2 2 0 01-2 2H22a2 2 0 01-2-2v-6a2 2 0 012-2z" stroke="#primary"/><path d="M5 33V7a2 2 0 012-2h38a2 2 0 012 2v23" stroke="#secondary" stroke-linecap="round"/><path d="M5 30v8c0 1.105.892 2 1.993 2H16" stroke="#secondary" stroke-linecap="round"/><g stroke="#primary" stroke-linecap="round"><path d="M26.432 37.933v7.07M26.432 37.933v9.07M24.432 40.433h7.07M24.432 40.433h8.07M24.432 44.433h7.07M24.432 44.433h8.07M30.432 37.933v9.07"/></g></g>',
...rank(value, [1, 20, 50, 100, 250]), ...rank(value, [1, 20, 50, 100, 250]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -116,12 +123,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = user.organizations.nodes?.shift() const unlock = user.organizations.nodes?.shift()
list.push({ list.push({
title:"Worker", title: "Worker",
text:`Joined ${value} organization${imports.s(value)}`, text: `Joined ${value} organization${imports.s(value)}`,
icon:'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#secondary" stroke-linejoin="round"><path d="M30 51H16.543v-2.998h-4v2.976l-5.537.016a2 2 0 01-2.006-2v-8.032a2 2 0 01.75-1.562l9.261-7.406 5.984 5.143m29.992 3.864v10h-6v-3h-5v3h-6m-.987-33c.133-1.116.793-2.106 1.978-2.968.44-.32 5.776-3.664 16.01-10.032v36"/><path d="M19 34.994v-8.982m16 0V49a2 2 0 01-2 2h-8.987l.011-6.957"/></g><path stroke="#secondary" d="M40 38h5M40 34h5"/><path stroke="#primary" d="M25 30h5M25 34h5M25 26h5"/><path d="M35.012 22.003H9.855a4.843 4.843 0 010-9.686h1.479c1.473-4.268 4.277-6.674 8.41-7.219 6.493-.856 9.767 4.27 10.396 5.9.734-.83 2.137-2.208 4.194-1.964a4.394 4.394 0 011.685.533" stroke="#primary" stroke-linejoin="round"/></g>', icon:
'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#secondary" stroke-linejoin="round"><path d="M30 51H16.543v-2.998h-4v2.976l-5.537.016a2 2 0 01-2.006-2v-8.032a2 2 0 01.75-1.562l9.261-7.406 5.984 5.143m29.992 3.864v10h-6v-3h-5v3h-6m-.987-33c.133-1.116.793-2.106 1.978-2.968.44-.32 5.776-3.664 16.01-10.032v36"/><path d="M19 34.994v-8.982m16 0V49a2 2 0 01-2 2h-8.987l.011-6.957"/></g><path stroke="#secondary" d="M40 38h5M40 34h5"/><path stroke="#primary" d="M25 30h5M25 34h5M25 26h5"/><path d="M35.012 22.003H9.855a4.843 4.843 0 010-9.686h1.479c1.473-4.268 4.277-6.674 8.41-7.219 6.493-.856 9.767 4.27 10.396 5.9.734-.83 2.137-2.208 4.194-1.964a4.394 4.394 0 011.685.533" stroke="#primary" stroke-linejoin="round"/></g>',
...rank(value, [1, 2, 4, 8, 10]), ...rank(value, [1, 2, 4, 8, 10]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -131,12 +139,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = user.starredRepositories.nodes?.shift() const unlock = user.starredRepositories.nodes?.shift()
list.push({ list.push({
title:"Stargazer", title: "Stargazer",
text:`Starred ${value} repositor${imports.s(value, "y")}`, text: `Starred ${value} repositor${imports.s(value, "y")}`,
icon:'<g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="none" fill-rule="evenodd"><path stroke="#primary" d="M28.017 5v3M36.006 7.013l-1.987 2.024M20.021 7.011l1.988 2.011M28.806 30.23c-2.206-3.88-5.25-2.234-5.25-2.234 1.007 2.24 1.688 3.72 2.742 8.724.957 4.551 3.785 7.409 7.687 7.293l5.028 6.003M29.03 34.057L29 20.007m4.012 9.004V17.005m4.006 11.99l-.003-9.353"/><path d="M18.993 50.038l4.045-5.993s1.03-.262 1.954-.984m-6.983.96c-4.474-.016-6.986-5.558-6.986-9.979 0-1.764-.439-4.997-1.997-8.004 0 0 3.268-1.24 5.747 3.6.904 1.768.458 5.267.642 5.388.185.121 1.336.554 2.637 2.01m4.955-18.92a976.92 976.92 0 010 5.91m-7.995-4.986l-.003 10.97M10.031 48.021l2.369-3.003" stroke="#secondary"/><path d="M45.996 47.026l-1.99-2.497-1.993-2.5s2.995-1.485 2.995-6.46V24.033" stroke="#primary"/><path d="M41 29v-6a2 2 0 114 0v2m-8-4v-4a2 2 0 114 0v7m-8-7v-2a2 2 0 114 0v2m-8 4v-2a2 2 0 114 0v2" stroke="#primary"/><path d="M23 20v-2a2 2 0 013.043-1.707M19 19v-4a2 2 0 114 0v3m-8 3v-2a2 2 0 114 0v10" stroke="#secondary"/><path d="M6.7 12c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.316-1.121-.572-1.372-1.71-1.678 1.135-.314 1.389-.567 1.7-1.69zm42 0c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689zM28.021 47.627c.317 1.122.573 1.372 1.71 1.678-1.135.314-1.389.566-1.699 1.69-.318-1.121-.573-1.372-1.71-1.679 1.134-.313 1.389-.566 1.699-1.689z" stroke="#primary"/></g>', icon:
'<g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="none" fill-rule="evenodd"><path stroke="#primary" d="M28.017 5v3M36.006 7.013l-1.987 2.024M20.021 7.011l1.988 2.011M28.806 30.23c-2.206-3.88-5.25-2.234-5.25-2.234 1.007 2.24 1.688 3.72 2.742 8.724.957 4.551 3.785 7.409 7.687 7.293l5.028 6.003M29.03 34.057L29 20.007m4.012 9.004V17.005m4.006 11.99l-.003-9.353"/><path d="M18.993 50.038l4.045-5.993s1.03-.262 1.954-.984m-6.983.96c-4.474-.016-6.986-5.558-6.986-9.979 0-1.764-.439-4.997-1.997-8.004 0 0 3.268-1.24 5.747 3.6.904 1.768.458 5.267.642 5.388.185.121 1.336.554 2.637 2.01m4.955-18.92a976.92 976.92 0 010 5.91m-7.995-4.986l-.003 10.97M10.031 48.021l2.369-3.003" stroke="#secondary"/><path d="M45.996 47.026l-1.99-2.497-1.993-2.5s2.995-1.485 2.995-6.46V24.033" stroke="#primary"/><path d="M41 29v-6a2 2 0 114 0v2m-8-4v-4a2 2 0 114 0v7m-8-7v-2a2 2 0 114 0v2m-8 4v-2a2 2 0 114 0v2" stroke="#primary"/><path d="M23 20v-2a2 2 0 013.043-1.707M19 19v-4a2 2 0 114 0v3m-8 3v-2a2 2 0 114 0v10" stroke="#secondary"/><path d="M6.7 12c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.316-1.121-.572-1.372-1.71-1.678 1.135-.314 1.389-.567 1.7-1.69zm42 0c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689zM28.021 47.627c.317 1.122.573 1.372 1.71 1.678-1.135.314-1.389.566-1.699 1.69-.318-1.121-.573-1.372-1.71-1.679 1.134-.313 1.389-.566 1.699-1.689z" stroke="#primary"/></g>',
...rank(value, [1, 200, 500, 1000, 2500]), ...rank(value, [1, 200, 500, 1000, 2500]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -146,12 +155,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = user.following.nodes?.shift() const unlock = user.following.nodes?.shift()
list.push({ list.push({
title:"Follower", title: "Follower",
text:`Following ${value} user${imports.s(value)}`, text: `Following ${value} user${imports.s(value)}`,
icon:'<g fill="none" fill-rule="evenodd"><path d="M35 31a7 7 0 1114 0 7 7 0 01-14 0zm12-13a3 3 0 116 0 3 3 0 01-6 0zM33 49a3 3 0 116 0 3 3 0 01-6 0zM4 15a3 3 0 116 0 3 3 0 01-6 0zm37-8.5a2.5 2.5 0 115 0 2.5 2.5 0 01-5 0zM10 14l4.029-.576M19.008 26.016L21 19M29.019 34.001l5.967-1.948M36.997 46.003l2.977-8.02M46.05 24.031L48 21M28.787 18.012l7.248 8.009" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M43.62 29.004c-1.157 0-1.437.676-1.62 1.173-.19-.498-.494-1.167-1.629-1.167-.909 0-1.355.777-1.371 1.632-.022 1.145 1.309 2.365 3 3.358 1.669-.983 3-2.23 3-3.358 0-.89-.54-1.638-1.38-1.638z" fill="#primary"/><path stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M48.043 15.003L45 9"/><path d="M21 12a3 3 0 116 0 3 3 0 01-6 0zM27 12h3M18 12h3M21 43c-.267-1.727-1.973-3-4-3-2.08 0-3.787 1.318-4 3m4-9a3 3 0 100 6 3 3 0 000-6z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M17 30a9 9 0 110 18 9 9 0 110-18z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g>', icon:
'<g fill="none" fill-rule="evenodd"><path d="M35 31a7 7 0 1114 0 7 7 0 01-14 0zm12-13a3 3 0 116 0 3 3 0 01-6 0zM33 49a3 3 0 116 0 3 3 0 01-6 0zM4 15a3 3 0 116 0 3 3 0 01-6 0zm37-8.5a2.5 2.5 0 115 0 2.5 2.5 0 01-5 0zM10 14l4.029-.576M19.008 26.016L21 19M29.019 34.001l5.967-1.948M36.997 46.003l2.977-8.02M46.05 24.031L48 21M28.787 18.012l7.248 8.009" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M43.62 29.004c-1.157 0-1.437.676-1.62 1.173-.19-.498-.494-1.167-1.629-1.167-.909 0-1.355.777-1.371 1.632-.022 1.145 1.309 2.365 3 3.358 1.669-.983 3-2.23 3-3.358 0-.89-.54-1.638-1.38-1.638z" fill="#primary"/><path stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M48.043 15.003L45 9"/><path d="M21 12a3 3 0 116 0 3 3 0 01-6 0zM27 12h3M18 12h3M21 43c-.267-1.727-1.973-3-4-3-2.08 0-3.787 1.318-4 3m4-9a3 3 0 100 6 3 3 0 000-6z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M17 30a9 9 0 110 18 9 9 0 110-18z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g>',
...rank(value, [1, 200, 500, 1000, 2500]), ...rank(value, [1, 200, 500, 1000, 2500]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -161,13 +171,14 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = user.followers.nodes?.shift() const unlock = user.followers.nodes?.shift()
list.push({ list.push({
title:"Influencer", title: "Influencer",
text:`Followed by ${value} user${imports.s(value)}`, text: `Followed by ${value} user${imports.s(value)}`,
icon:'<g transform="translate(4 4)" stroke-width="2" fill="none" fill-rule="evenodd"><path d="M33.432 1.924A23.922 23.922 0 0024 0c-3.945 0-7.668.952-10.95 2.638m-9.86 9.398A23.89 23.89 0 000 24a23.9 23.9 0 002.274 10.21m3.45 5.347a23.992 23.992 0 0012.929 7.845m13.048-.664c4.43-1.5 8.28-4.258 11.123-7.848m3.16-5.245A23.918 23.918 0 0048 24c0-1.87-.214-3.691-.619-5.439M40.416 6.493a24.139 24.139 0 00-1.574-1.355" stroke="#secondary" stroke-linecap="round"/><path stroke="#secondary" d="M4.582 33.859l1.613-7.946"/><circle stroke="#secondary" cx="6.832" cy="23" r="3"/><path stroke="#primary" d="M17.444 39.854l4.75 3.275"/><path stroke="#secondary" stroke-linecap="round" d="M7.647 14.952l-.433 4.527"/><circle stroke="#primary" cx="15" cy="38" r="3"/><path stroke="#primary" d="M22.216 9.516l.455 4.342"/><path stroke="#secondary" stroke-linecap="round" d="M34.272 6.952l-2.828 5.25"/><path stroke="#primary" stroke-linecap="square" d="M11.873 7.235l6.424-.736"/><path stroke="#secondary" stroke-linecap="round" d="M28.811 5.445l3.718-.671"/><path stroke="#primary" d="M42.392 22.006l.456-5.763M34.349 24.426l4.374.447"/><path d="M20 28c.267-1.727 1.973-3 4-3 2.08 0 3.787 1.318 4 3m-4-9a3 3 0 110 6 3 3 0 010-6z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path d="M24 14c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><circle stroke="#secondary" cx="35.832" cy="4" r="3"/><circle stroke="#secondary" cx="44" cy="36" r="3"/><circle stroke="#secondary" cx="34.832" cy="37" r="3"/><circle stroke="#primary" cx="21.654" cy="6.437" r="3"/><path d="M25.083 48.102a3 3 0 100-6 3 3 0 000 6z" stroke="#primary"/><path d="M8.832 5a3 3 0 110 6 3 3 0 010-6z" stroke="#primary" stroke-linecap="round"/><circle stroke="#secondary" cx="4" cy="37" r="3"/><path d="M42.832 10a3 3 0 110 6 3 3 0 010-6z" stroke="#primary" stroke-linecap="round"/><path stroke="#secondary" stroke-linecap="round" d="M32.313 38.851l-1.786 1.661"/><circle stroke="#primary" cx="42" cy="25" r="3"/><path stroke="#primary" stroke-linecap="square" d="M18.228 32.388l-1.562 2.66"/><path stroke="#secondary" d="M37.831 36.739l2.951-.112"/></g>', icon:
'<g transform="translate(4 4)" stroke-width="2" fill="none" fill-rule="evenodd"><path d="M33.432 1.924A23.922 23.922 0 0024 0c-3.945 0-7.668.952-10.95 2.638m-9.86 9.398A23.89 23.89 0 000 24a23.9 23.9 0 002.274 10.21m3.45 5.347a23.992 23.992 0 0012.929 7.845m13.048-.664c4.43-1.5 8.28-4.258 11.123-7.848m3.16-5.245A23.918 23.918 0 0048 24c0-1.87-.214-3.691-.619-5.439M40.416 6.493a24.139 24.139 0 00-1.574-1.355" stroke="#secondary" stroke-linecap="round"/><path stroke="#secondary" d="M4.582 33.859l1.613-7.946"/><circle stroke="#secondary" cx="6.832" cy="23" r="3"/><path stroke="#primary" d="M17.444 39.854l4.75 3.275"/><path stroke="#secondary" stroke-linecap="round" d="M7.647 14.952l-.433 4.527"/><circle stroke="#primary" cx="15" cy="38" r="3"/><path stroke="#primary" d="M22.216 9.516l.455 4.342"/><path stroke="#secondary" stroke-linecap="round" d="M34.272 6.952l-2.828 5.25"/><path stroke="#primary" stroke-linecap="square" d="M11.873 7.235l6.424-.736"/><path stroke="#secondary" stroke-linecap="round" d="M28.811 5.445l3.718-.671"/><path stroke="#primary" d="M42.392 22.006l.456-5.763M34.349 24.426l4.374.447"/><path d="M20 28c.267-1.727 1.973-3 4-3 2.08 0 3.787 1.318 4 3m-4-9a3 3 0 110 6 3 3 0 010-6z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path d="M24 14c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><circle stroke="#secondary" cx="35.832" cy="4" r="3"/><circle stroke="#secondary" cx="44" cy="36" r="3"/><circle stroke="#secondary" cx="34.832" cy="37" r="3"/><circle stroke="#primary" cx="21.654" cy="6.437" r="3"/><path d="M25.083 48.102a3 3 0 100-6 3 3 0 000 6z" stroke="#primary"/><path d="M8.832 5a3 3 0 110 6 3 3 0 010-6z" stroke="#primary" stroke-linecap="round"/><circle stroke="#secondary" cx="4" cy="37" r="3"/><path d="M42.832 10a3 3 0 110 6 3 3 0 010-6z" stroke="#primary" stroke-linecap="round"/><path stroke="#secondary" stroke-linecap="round" d="M32.313 38.851l-1.786 1.661"/><circle stroke="#primary" cx="42" cy="25" r="3"/><path stroke="#primary" stroke-linecap="square" d="M18.228 32.388l-1.562 2.66"/><path stroke="#secondary" d="M37.831 36.739l2.951-.112"/></g>',
...rank(value, [1, 200, 500, 1000, 2500]), ...rank(value, [1, 200, 500, 1000, 2500]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
leaderboard:leaderboard({user:ranks.user_rank.userCount, requirement:scores.followers >= requirements.followers, type:"users"}), leaderboard: leaderboard({user: ranks.user_rank.userCount, requirement: scores.followers >= requirements.followers, type: "users"}),
}) })
} }
@@ -177,13 +188,14 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = null const unlock = null
list.push({ list.push({
title:"Maintainer", title: "Maintainer",
text:`Maintaining a repository with ${value} star${imports.s(value)}`, text: `Maintaining a repository with ${value} star${imports.s(value)}`,
icon:'<g transform="translate(4 4)" fill="none" fill-rule="evenodd"><path d="M39 15h.96l4.038 3-.02-3H45a2 2 0 002-2V3a2 2 0 00-2-2H31a2 2 0 00-2 2v4.035" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M36 5.014l-3 3 3 3M40 5.014l3 3-3 3"/><path d="M6 37a1 1 0 110 2 1 1 0 010-2m7 0a1 1 0 110 2 1 1 0 010-2m-2.448 1a1 1 0 11-2 0 1 1 0 012 0z" fill="#primary"/><path d="M1.724 15.05A23.934 23.934 0 000 24c0 .686.029 1.366.085 2.037m19.92 21.632c1.3.218 2.634.331 3.995.331a23.92 23.92 0 009.036-1.76m13.207-13.21A23.932 23.932 0 0048 24c0-1.363-.114-2.7-.332-4M25.064.022a23.932 23.932 0 00-10.073 1.725" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M19 42.062V43a2 2 0 01-2 2H9.04l-4.038 3 .02-3H3a2 2 0 01-2-2V33a2 2 0 012-2h4.045" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M6 0a6 6 0 110 12A6 6 0 016 0z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" d="M6 3v6M3 6h6"/><path d="M42 36a6 6 0 110 12 6 6 0 010-12z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M44.338 40.663l-3.336 3.331-1.692-1.686M31 31c-.716-2.865-3.578-5-7-5-3.423 0-6.287 2.14-7 5"/><path d="M24 16a5 5 0 110 10 5 5 0 010-10z" stroke="#primary" stroke-width="2" stroke-linecap="round"/><circle stroke="#primary" stroke-width="2" cx="24" cy="24" r="14"/></g>', icon:
'<g transform="translate(4 4)" fill="none" fill-rule="evenodd"><path d="M39 15h.96l4.038 3-.02-3H45a2 2 0 002-2V3a2 2 0 00-2-2H31a2 2 0 00-2 2v4.035" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M36 5.014l-3 3 3 3M40 5.014l3 3-3 3"/><path d="M6 37a1 1 0 110 2 1 1 0 010-2m7 0a1 1 0 110 2 1 1 0 010-2m-2.448 1a1 1 0 11-2 0 1 1 0 012 0z" fill="#primary"/><path d="M1.724 15.05A23.934 23.934 0 000 24c0 .686.029 1.366.085 2.037m19.92 21.632c1.3.218 2.634.331 3.995.331a23.92 23.92 0 009.036-1.76m13.207-13.21A23.932 23.932 0 0048 24c0-1.363-.114-2.7-.332-4M25.064.022a23.932 23.932 0 00-10.073 1.725" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M19 42.062V43a2 2 0 01-2 2H9.04l-4.038 3 .02-3H3a2 2 0 01-2-2V33a2 2 0 012-2h4.045" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M6 0a6 6 0 110 12A6 6 0 016 0z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" d="M6 3v6M3 6h6"/><path d="M42 36a6 6 0 110 12 6 6 0 010-12z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M44.338 40.663l-3.336 3.331-1.692-1.686M31 31c-.716-2.865-3.578-5-7-5-3.423 0-6.287 2.14-7 5"/><path d="M24 16a5 5 0 110 10 5 5 0 010-10z" stroke="#primary" stroke-width="2" stroke-linecap="round"/><circle stroke="#primary" stroke-width="2" cx="24" cy="24" r="14"/></g>',
...rank(value, [1, 1000, 5000, 10000, 25000]), ...rank(value, [1, 1000, 5000, 10000, 25000]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
leaderboard:leaderboard({user:ranks.repo_rank.repositoryCount, requirement:scores.stars >= requirements.stars, type:"repositories"}), leaderboard: leaderboard({user: ranks.repo_rank.repositoryCount, requirement: scores.stars >= requirements.stars, type: "repositories"}),
}) })
} }
@@ -192,43 +204,46 @@ export default async function({list, login, data, computed, imports, graphql, qu
const value = Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount)) const value = Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount))
const unlock = null const unlock = null
list.push({ list.push({
title:"Inspirer", title: "Inspirer",
text:`Maintaining or created a repository which has been forked ${value} time${imports.s(value)}`, text: `Maintaining or created a repository which has been forked ${value} time${imports.s(value)}`,
icon:'<g transform="translate(4 4)" fill="none" fill-rule="evenodd"><path d="M20.065 47.122c.44-.525.58-1.448.58-1.889 0-2.204-1.483-3.967-3.633-4.187.447-1.537.58-2.64.397-3.31-.25-.92-.745-1.646-1.409-2.235m-5.97-7.157c.371-.254.911-.748 1.62-1.48a8.662 8.662 0 001.432-2.366M47 22h-7c-1.538 0-2.749-.357-4-1h-5c-1.789.001-3-1.3-3-2.955 0-1.656 1.211-3.04 3-3.045h2c.027-1.129.513-2.17 1-3m3.082 32.004C34.545 43.028 34.02 40.569 34 39v-1h-1c-2.603-.318-5-2.913-5-5.997S30.397 26 33 26h9c2.384 0 4.326 1.024 5.27 3" stroke="#secondary" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><g transform="translate(36)" stroke="#primary" stroke-width="2"><path fill="#primary" stroke-linecap="round" stroke-linejoin="round" d="M5.395 5.352L6.009 4l.598 1.348L8 5.408l-1.067 1.12.425 1.47-1.356-.908-1.35.91.404-1.469L4 5.41z"/><circle cx="6" cy="6" r="6"/></g><g transform="translate(0 31)" stroke="#primary" stroke-width="2"><circle cx="6" cy="6" r="6"/><g stroke-linecap="round"><path d="M6 4v4M4 6h4"/></g></g><circle stroke="#primary" stroke-width="2" cx="10.5" cy="10.5" r="10.5"/><g stroke-linecap="round"><path d="M32.01 1.37A23.96 23.96 0 0024 0c-.999 0-1.983.061-2.95.18M.32 20.072a24.21 24.21 0 00.015 7.948M12.42 45.025A23.892 23.892 0 0024 48c13.255 0 24-10.745 24-24 0-2.811-.483-5.51-1.371-8.016" stroke="#secondary" stroke-width="2"/><path stroke="#primary" stroke-width="2" d="M8.999 7.151v5.865"/><path d="M9 3a2 2 0 110 4 2 2 0 010-4zm0 10.8a2 2 0 11-.001 4 2 2 0 01.001-4z" stroke="#primary" stroke-width="1.8"/><path d="M9.622 11.838c.138-.007.989.119 1.595-.05.607-.169 1.584-.539 1.829-1.337" stroke="#primary" stroke-width="2"/><path d="M14.8 7.202a2 2 0 110 4 2 2 0 010-4z" stroke="#primary" stroke-width="1.8"/></g></g>', icon:
'<g transform="translate(4 4)" fill="none" fill-rule="evenodd"><path d="M20.065 47.122c.44-.525.58-1.448.58-1.889 0-2.204-1.483-3.967-3.633-4.187.447-1.537.58-2.64.397-3.31-.25-.92-.745-1.646-1.409-2.235m-5.97-7.157c.371-.254.911-.748 1.62-1.48a8.662 8.662 0 001.432-2.366M47 22h-7c-1.538 0-2.749-.357-4-1h-5c-1.789.001-3-1.3-3-2.955 0-1.656 1.211-3.04 3-3.045h2c.027-1.129.513-2.17 1-3m3.082 32.004C34.545 43.028 34.02 40.569 34 39v-1h-1c-2.603-.318-5-2.913-5-5.997S30.397 26 33 26h9c2.384 0 4.326 1.024 5.27 3" stroke="#secondary" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/><g transform="translate(36)" stroke="#primary" stroke-width="2"><path fill="#primary" stroke-linecap="round" stroke-linejoin="round" d="M5.395 5.352L6.009 4l.598 1.348L8 5.408l-1.067 1.12.425 1.47-1.356-.908-1.35.91.404-1.469L4 5.41z"/><circle cx="6" cy="6" r="6"/></g><g transform="translate(0 31)" stroke="#primary" stroke-width="2"><circle cx="6" cy="6" r="6"/><g stroke-linecap="round"><path d="M6 4v4M4 6h4"/></g></g><circle stroke="#primary" stroke-width="2" cx="10.5" cy="10.5" r="10.5"/><g stroke-linecap="round"><path d="M32.01 1.37A23.96 23.96 0 0024 0c-.999 0-1.983.061-2.95.18M.32 20.072a24.21 24.21 0 00.015 7.948M12.42 45.025A23.892 23.892 0 0024 48c13.255 0 24-10.745 24-24 0-2.811-.483-5.51-1.371-8.016" stroke="#secondary" stroke-width="2"/><path stroke="#primary" stroke-width="2" d="M8.999 7.151v5.865"/><path d="M9 3a2 2 0 110 4 2 2 0 010-4zm0 10.8a2 2 0 11-.001 4 2 2 0 01.001-4z" stroke="#primary" stroke-width="1.8"/><path d="M9.622 11.838c.138-.007.989.119 1.595-.05.607-.169 1.584-.539 1.829-1.337" stroke="#primary" stroke-width="2"/><path d="M14.8 7.202a2 2 0 110 4 2 2 0 010-4z" stroke="#primary" stroke-width="1.8"/></g></g>',
...rank(value, [1, 100, 500, 1000, 2500]), ...rank(value, [1, 100, 500, 1000, 2500]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
leaderboard:leaderboard({user:ranks.forks_rank.repositoryCount, requirement:scores.forks >= requirements.forks, type:"repositories"}), leaderboard: leaderboard({user: ranks.forks_rank.repositoryCount, requirement: scores.forks >= requirements.forks, type: "repositories"}),
}) })
} }
//Polyglot //Polyglot
{ {
const value = new Set(data.user.repositories.nodes.flatMap(repository => repository.languages.edges.map(({node:{name}}) => name))).size const value = new Set(data.user.repositories.nodes.flatMap(repository => repository.languages.edges.map(({node: {name}}) => name))).size
const unlock = null const unlock = null
list.push({ list.push({
title:"Polyglot", title: "Polyglot",
text:`Using ${value} different programming language${imports.s(value)}`, text: `Using ${value} different programming language${imports.s(value)}`,
icon:'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><path d="M17.135 7.988l-3.303.669a2 2 0 00-1.586 2.223l4.708 35.392a1.498 1.498 0 01-1.162 1.66 1.523 1.523 0 01-1.775-1.01L4.951 19.497a2 2 0 011.215-2.507l2.946-1.072" stroke="#secondary" stroke-linejoin="round"/><path d="M36.8 48H23a2 2 0 01-2-2V7a2 2 0 012-2h26a2 2 0 012 2v32.766" stroke="#primary"/><path d="M29 20.955l-3.399 3.399a.85.85 0 000 1.202l3.399 3.4M43.014 20.955l3.399 3.399a.85.85 0 010 1.202l-3.4 3.4" stroke="#primary" stroke-linejoin="round"/><path stroke="#primary" d="M38.526 18l-5.053 14.016"/><path d="M44 36a8 8 0 110 16 8 8 0 010-16z" stroke="#primary" stroke-linejoin="round"/><path d="M43.068 40.749l3.846 2.396a1 1 0 01-.006 1.7l-3.846 2.36a1 1 0 01-1.523-.853v-4.755a1 1 0 011.529-.848z" stroke="#primary" stroke-linejoin="round"/></g>', icon:
'<g stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><path d="M17.135 7.988l-3.303.669a2 2 0 00-1.586 2.223l4.708 35.392a1.498 1.498 0 01-1.162 1.66 1.523 1.523 0 01-1.775-1.01L4.951 19.497a2 2 0 011.215-2.507l2.946-1.072" stroke="#secondary" stroke-linejoin="round"/><path d="M36.8 48H23a2 2 0 01-2-2V7a2 2 0 012-2h26a2 2 0 012 2v32.766" stroke="#primary"/><path d="M29 20.955l-3.399 3.399a.85.85 0 000 1.202l3.399 3.4M43.014 20.955l3.399 3.399a.85.85 0 010 1.202l-3.4 3.4" stroke="#primary" stroke-linejoin="round"/><path stroke="#primary" d="M38.526 18l-5.053 14.016"/><path d="M44 36a8 8 0 110 16 8 8 0 010-16z" stroke="#primary" stroke-linejoin="round"/><path d="M43.068 40.749l3.846 2.396a1 1 0 01-.006 1.7l-3.846 2.36a1 1 0 01-1.523-.853v-4.755a1 1 0 011.529-.848z" stroke="#primary" stroke-linejoin="round"/></g>',
...rank(value, [1, 4, 8, 16, 32]), ...rank(value, [1, 4, 8, 16, 32]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
//Member //Member
{ {
const {years:value} = computed.registered const {years: value} = computed.registered
const unlock = null const unlock = null
list.push({ list.push({
title:"Member", title: "Member",
text:`Registered ${Math.floor(value)} year${imports.s(Math.floor(value))} ago`, text: `Registered ${Math.floor(value)} year${imports.s(Math.floor(value))} ago`,
icon:'<g xmlns="http://www.w3.org/2000/svg" transform="translate(5 4)" fill="none" fill-rule="evenodd"><path d="M46 44.557v1a2 2 0 01-2 2H2a2 2 0 01-2-2v-1" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M.75 40.993l.701.561a2.323 2.323 0 002.903 0l1.675-1.34a3 3 0 013.748 0l1.282 1.026a3 3 0 003.71.03l1.4-1.085a3 3 0 013.75.061l1.103.913a3 3 0 003.787.031l1.22-.976a3 3 0 013.748 0l1.282 1.026a3 3 0 003.71.03l1.4-1.085a3 3 0 013.75.061l1.429 1.182a2.427 2.427 0 003.103-.008l.832-.695A2 2 0 0046 39.191v-1.634a2 2 0 00-2-2H2a2 2 0 00-2 2v1.875a2 2 0 00.75 1.561z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M42 31.609v.948m-38 0v-.992m25.04-15.008H35a2 2 0 012 2v1m-28 0v-1a2 2 0 012-2h6.007" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 8.557h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6a1 1 0 011-1z" stroke="#primary" stroke-width="2" stroke-linejoin="round"/><path d="M4.7 10.557c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689zm35-8c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M23 5.557a2 2 0 002-2C25 2.452 24.433 0 22.273 0c-.463 0 .21 1.424-.502 1.979A2 2 0 0023 5.557z" stroke="#primary" stroke-width="2"/><path d="M4.78 27.982l1.346 1.076a3 3 0 003.748 0l1.252-1.002a3 3 0 013.748 0l1.282 1.026a3 3 0 003.711.03l1.4-1.085a3 3 0 013.75.061l1.102.913a3 3 0 003.787.031l1.22-.976a3 3 0 013.748 0l1.281 1.025a3 3 0 003.712.029l1.358-1.053a2 2 0 00.775-1.58v-.97a1.95 1.95 0 00-1.95-1.95H5.942a1.912 1.912 0 00-1.912 1.912v.951a2 2 0 00.75 1.562z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle stroke="#secondary" cx="16.5" cy="2.057" r="1"/><circle stroke="#secondary" cx="14.5" cy="12.057" r="1"/><circle stroke="#secondary" cx="31.5" cy="9.057" r="1"/></g>', icon:
'<g xmlns="http://www.w3.org/2000/svg" transform="translate(5 4)" fill="none" fill-rule="evenodd"><path d="M46 44.557v1a2 2 0 01-2 2H2a2 2 0 01-2-2v-1" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M.75 40.993l.701.561a2.323 2.323 0 002.903 0l1.675-1.34a3 3 0 013.748 0l1.282 1.026a3 3 0 003.71.03l1.4-1.085a3 3 0 013.75.061l1.103.913a3 3 0 003.787.031l1.22-.976a3 3 0 013.748 0l1.282 1.026a3 3 0 003.71.03l1.4-1.085a3 3 0 013.75.061l1.429 1.182a2.427 2.427 0 003.103-.008l.832-.695A2 2 0 0046 39.191v-1.634a2 2 0 00-2-2H2a2 2 0 00-2 2v1.875a2 2 0 00.75 1.561z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M42 31.609v.948m-38 0v-.992m25.04-15.008H35a2 2 0 012 2v1m-28 0v-1a2 2 0 012-2h6.007" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M22 8.557h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6a1 1 0 011-1z" stroke="#primary" stroke-width="2" stroke-linejoin="round"/><path d="M4.7 10.557c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689zm35-8c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M23 5.557a2 2 0 002-2C25 2.452 24.433 0 22.273 0c-.463 0 .21 1.424-.502 1.979A2 2 0 0023 5.557z" stroke="#primary" stroke-width="2"/><path d="M4.78 27.982l1.346 1.076a3 3 0 003.748 0l1.252-1.002a3 3 0 013.748 0l1.282 1.026a3 3 0 003.711.03l1.4-1.085a3 3 0 013.75.061l1.102.913a3 3 0 003.787.031l1.22-.976a3 3 0 013.748 0l1.281 1.025a3 3 0 003.712.029l1.358-1.053a2 2 0 00.775-1.58v-.97a1.95 1.95 0 00-1.95-1.95H5.942a1.912 1.912 0 00-1.912 1.912v.951a2 2 0 00.75 1.562z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle stroke="#secondary" cx="16.5" cy="2.057" r="1"/><circle stroke="#secondary" cx="14.5" cy="12.057" r="1"/><circle stroke="#secondary" cx="31.5" cy="9.057" r="1"/></g>',
...rank(value, [1, 3, 5, 10, 15]), ...rank(value, [1, 3, 5, 10, 15]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -238,12 +253,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = null const unlock = null
list.push({ list.push({
title:"Sponsor", title: "Sponsor",
text:`Sponsoring ${value} user${imports.s(value)} or organization${imports.s(value)}`, text: `Sponsoring ${value} user${imports.s(value)} or organization${imports.s(value)}`,
icon:'<g xmlns="http://www.w3.org/2000/svg" fill="none" fill-rule="evenodd"><path d="M24 32c.267-1.727 1.973-3 4-3 2.08 0 3.787 1.318 4 3m-4-9a3 3 0 110 6 3 3 0 010-6z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M28 18c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M46.138 15c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C41.347 15 41 16.117 41 17.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005zm-31-5c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C10.347 10 10 11.117 10 12.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005zm6 32c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C16.347 42 16 43.117 16 44.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005z" fill="#secondary"/><path d="M8.003 29a3 3 0 110 6 3 3 0 010-6zM32.018 5.005a3 3 0 110 6 3 3 0 010-6z" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path stroke="#secondary" stroke-width="2" d="M29.972 18.026L31.361 11M18.063 29.987l-7.004 1.401"/><path d="M22.604 11.886l.746 2.164m-9.313 9.296l-2.156-.712" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M21.304 9a1 1 0 100-2 1 1 0 000 2zM8.076 22.346a1 1 0 100-2 1 1 0 000 2z" fill="#primary"/><path d="M33.267 44.17l-.722-2.146m9.38-9.206l2.147.743" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M34.544 49.031a1 1 0 100-2 1 1 0 000 2zm13.314-13.032a1 1 0 100-2 1 1 0 000 2z" fill="#primary"/><path d="M48.019 51.004a3 3 0 100-6 3 3 0 000 6zM35.194 35.33l10.812 11.019" stroke="#secondary" stroke-width="2"/></g>', icon:
'<g xmlns="http://www.w3.org/2000/svg" fill="none" fill-rule="evenodd"><path d="M24 32c.267-1.727 1.973-3 4-3 2.08 0 3.787 1.318 4 3m-4-9a3 3 0 110 6 3 3 0 010-6z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M28 18c5.523 0 10 4.477 10 10s-4.477 10-10 10-10-4.477-10-10 4.477-10 10-10z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M46.138 15c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C41.347 15 41 16.117 41 17.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005zm-31-5c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C10.347 10 10 11.117 10 12.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005zm6 32c-1.033 0-1.454.822-1.634 1.413-.019.06-.024.06-.042 0-.182-.591-.707-1.413-1.655-1.413C16.347 42 16 43.117 16 44.005c0 1.676 2.223 3.228 3.091 3.845.272.197.556.194.817 0 .798-.593 3.092-2.17 3.092-3.845 0-.888-.261-2.005-1.862-2.005z" fill="#secondary"/><path d="M8.003 29a3 3 0 110 6 3 3 0 010-6zM32.018 5.005a3 3 0 110 6 3 3 0 010-6z" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path stroke="#secondary" stroke-width="2" d="M29.972 18.026L31.361 11M18.063 29.987l-7.004 1.401"/><path d="M22.604 11.886l.746 2.164m-9.313 9.296l-2.156-.712" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M21.304 9a1 1 0 100-2 1 1 0 000 2zM8.076 22.346a1 1 0 100-2 1 1 0 000 2z" fill="#primary"/><path d="M33.267 44.17l-.722-2.146m9.38-9.206l2.147.743" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M34.544 49.031a1 1 0 100-2 1 1 0 000 2zm13.314-13.032a1 1 0 100-2 1 1 0 000 2z" fill="#primary"/><path d="M48.019 51.004a3 3 0 100-6 3 3 0 000 6zM35.194 35.33l10.812 11.019" stroke="#secondary" stroke-width="2"/></g>',
...rank(value, [1, 3, 5, 10, 25]), ...rank(value, [1, 3, 5, 10, 25]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -253,12 +269,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = null const unlock = null
list.push({ list.push({
title:"Deployer", title: "Deployer",
text:`Repositories have been deployed ${value} time${imports.s(value)}`, text: `Repositories have been deployed ${value} time${imports.s(value)}`,
icon:'<g stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#secondary"><path d="M11 40a2 2 0 100-4 2 2 0 000 4z"/><path d="M11 34v1m0 5v3m0 3v3" stroke-linecap="round"/></g><g stroke="#secondary" stroke-linecap="round" stroke-linejoin="round"><path d="M47.01 41.009h-4M45.016 39v4"/></g><path d="M27.982 5c2.79 1.873 4.46 5.876 5.008 12.01l2.059.659a1.606 1.606 0 011.606-1.665h.84a2.513 2.513 0 012.511 2.508l.004 1.496 3.197 1.588a1.951 1.951 0 011.684-.952l.509.002c.898.003 1.625.73 1.629 1.629l.008 2.115L51 27.606v1.945l-4.815-1.211c-.474.894-.87 1.48-1.192 1.757-.345-.328-.814-1.158-1.406-2.49L38.744 26.5c-.402 1.153-.845 1.828-1.328 2.026-.451-.444-1.04-1.55-1.409-2.821-1.481-.286-2.486-.56-2.994-.688-.27 2.397-1.036 6.884-2.009 10.982l5.006 4.438-6.555-1.08-1.454 3.654-1.45-3.658-6.56 1.082 4.996-4.417c-.899-4.02-1.576-7.684-2.03-10.992a37.29 37.29 0 01-2.967.679c-.38 1.252-.845 2.191-1.396 2.817-.63-.184-1.142-1.474-1.338-2.023-.705.15-2.323.519-4.853 1.107-.601 1.388-1.07 2.218-1.41 2.49a7.032 7.032 0 01-1.173-1.758L5 29.55v-1.945l3.99-3.265v-2.102a1.604 1.604 0 011.625-1.604l.528.007c.68.008 1.307.37 1.654.956l3.184-1.614.003-1.467a2.503 2.503 0 012.511-2.497l.863.003a1.6 1.6 0 011.594 1.646 62.42 62.42 0 012.024-.667c.572-6.097 2.24-10.098 5.006-12.002z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#secondary" stroke-linecap="round" d="M45.016 36.032v-2M45.016 49.032v-3M38.978 36.089v-3.153M17.016 36.089v-3.153M51.031 51.165v-2.193m0-2.972V35.013M4.974 51.165v-2.193m0-2.972V35.013"/></g>', icon:
'<g stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#secondary"><path d="M11 40a2 2 0 100-4 2 2 0 000 4z"/><path d="M11 34v1m0 5v3m0 3v3" stroke-linecap="round"/></g><g stroke="#secondary" stroke-linecap="round" stroke-linejoin="round"><path d="M47.01 41.009h-4M45.016 39v4"/></g><path d="M27.982 5c2.79 1.873 4.46 5.876 5.008 12.01l2.059.659a1.606 1.606 0 011.606-1.665h.84a2.513 2.513 0 012.511 2.508l.004 1.496 3.197 1.588a1.951 1.951 0 011.684-.952l.509.002c.898.003 1.625.73 1.629 1.629l.008 2.115L51 27.606v1.945l-4.815-1.211c-.474.894-.87 1.48-1.192 1.757-.345-.328-.814-1.158-1.406-2.49L38.744 26.5c-.402 1.153-.845 1.828-1.328 2.026-.451-.444-1.04-1.55-1.409-2.821-1.481-.286-2.486-.56-2.994-.688-.27 2.397-1.036 6.884-2.009 10.982l5.006 4.438-6.555-1.08-1.454 3.654-1.45-3.658-6.56 1.082 4.996-4.417c-.899-4.02-1.576-7.684-2.03-10.992a37.29 37.29 0 01-2.967.679c-.38 1.252-.845 2.191-1.396 2.817-.63-.184-1.142-1.474-1.338-2.023-.705.15-2.323.519-4.853 1.107-.601 1.388-1.07 2.218-1.41 2.49a7.032 7.032 0 01-1.173-1.758L5 29.55v-1.945l3.99-3.265v-2.102a1.604 1.604 0 011.625-1.604l.528.007c.68.008 1.307.37 1.654.956l3.184-1.614.003-1.467a2.503 2.503 0 012.511-2.497l.863.003a1.6 1.6 0 011.594 1.646 62.42 62.42 0 012.024-.667c.572-6.097 2.24-10.098 5.006-12.002z" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#secondary" stroke-linecap="round" d="M45.016 36.032v-2M45.016 49.032v-3M38.978 36.089v-3.153M17.016 36.089v-3.153M51.031 51.165v-2.193m0-2.972V35.013M4.974 51.165v-2.193m0-2.972V35.013"/></g>',
...rank(value, [1, 200, 500, 1000, 2500]), ...rank(value, [1, 200, 500, 1000, 2500]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -268,12 +285,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = null const unlock = null
list.push({ list.push({
title:"Chatter", title: "Chatter",
text:`Participated in discussions ${value} time${imports.s(value)}`, text: `Participated in discussions ${value} time${imports.s(value)}`,
icon:'<g fill="none" fill-rule="evenodd"><path d="M6 42c.45-3.415 3.34-6 7-6 1.874 0 3.752.956 5 3m-6-13a5 5 0 110 10 5 5 0 010-10zm38 16c-.452-3.415-3.34-6-7-6-1.874 0-3.752.956-5 3m6-13a5 5 0 100 10 5 5 0 000-10z" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M37 51c-.92-4.01-4.6-7-9-7-4.401 0-8.083 2.995-9 7" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M28.01 31.004a6.5 6.5 0 110 13 6.5 6.5 0 010-13z" stroke="#primary" stroke-width="2" stroke-linecap="round"/><path d="M28 14.011a5 5 0 11-5 4.998 5 5 0 015-4.998z" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M22 26c1.558-1.25 3.665-2 6-2 2.319 0 4.439.761 6 2" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M51 9V8c0-1.3-1.574-3-3-3h-8c-1.426 0-3 1.7-3 3v13l4-4h6c2.805-.031 4-1.826 4-4V9zM5 9V8c0-1.3 1.574-3 3-3h8c1.426 0 3 1.7 3 3v13l-4-4H9c-2.805-.031-4-1.826-4-4V9z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M43 11a1 1 0 11-2 0 1 1 0 012 0zm4 0a1 1 0 11-2 0 1 1 0 012 0zm-36 0a1 1 0 11-2 0 1 1 0 012 0zm4 0a1 1 0 11-2 0 1 1 0 012 0z" fill="#primary"/></g>', icon:
'<g fill="none" fill-rule="evenodd"><path d="M6 42c.45-3.415 3.34-6 7-6 1.874 0 3.752.956 5 3m-6-13a5 5 0 110 10 5 5 0 010-10zm38 16c-.452-3.415-3.34-6-7-6-1.874 0-3.752.956-5 3m6-13a5 5 0 100 10 5 5 0 000-10z" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M37 51c-.92-4.01-4.6-7-9-7-4.401 0-8.083 2.995-9 7" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M28.01 31.004a6.5 6.5 0 110 13 6.5 6.5 0 010-13z" stroke="#primary" stroke-width="2" stroke-linecap="round"/><path d="M28 14.011a5 5 0 11-5 4.998 5 5 0 015-4.998z" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M22 26c1.558-1.25 3.665-2 6-2 2.319 0 4.439.761 6 2" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M51 9V8c0-1.3-1.574-3-3-3h-8c-1.426 0-3 1.7-3 3v13l4-4h6c2.805-.031 4-1.826 4-4V9zM5 9V8c0-1.3 1.574-3 3-3h8c1.426 0 3 1.7 3 3v13l-4-4H9c-2.805-.031-4-1.826-4-4V9z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M43 11a1 1 0 11-2 0 1 1 0 012 0zm4 0a1 1 0 11-2 0 1 1 0 012 0zm-36 0a1 1 0 11-2 0 1 1 0 012 0zm4 0a1 1 0 11-2 0 1 1 0 012 0z" fill="#primary"/></g>',
...rank(value, [1, 200, 500, 1000, 2500]), ...rank(value, [1, 200, 500, 1000, 2500]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -283,12 +301,13 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = null const unlock = null
list.push({ list.push({
title:"Helper", title: "Helper",
text:`Answered and solved ${value} discussion${imports.s(value)}`, text: `Answered and solved ${value} discussion${imports.s(value)}`,
icon:'<g xmlns="http://www.w3.org/2000/svg" fill="none" fill-rule="evenodd"><path d="M28 37c1.003.005.997-.443 1-1 .004-1.458-.004-4.629 0-6-.007-.564-.068-.987-1-1-2.118 0-4 1.79-4 4s1.882 4 4 4zM48 37c-1.003.005-.997-.443-1-1-.004-1.458.004-4.629 0-6 .007-.564.068-.987 1-1 2.118 0 4 1.79 4 4s-1.882 4-4 4z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M26 51c.798-4.48 4.87-8.082 9.97-8.849.66-.1 1.338-.151 2.028-.151m9.606 4.038c1.27 1.408 2.12 3.102 2.396 4.945M32.002 39.71A8.966 8.966 0 0038 42a8.967 8.967 0 006.02-2.31m-.015-12.394A8.967 8.967 0 0038 25a8.966 8.966 0 00-5.994 2.286" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M38 45c.17 0 .34-.004.509-.01 5.23-.219 9.596-3.785 11.01-8.613m-.004-6.766C48.052 24.634 43.45 21 38 21c-5.485 0-10.11 3.68-11.542 8.706" stroke="#primary" stroke-width="2"/><path d="M37.85 44h1.173c.54 0 .977.438.977.977v.523a1.5 1.5 0 01-3 0v-.65c0-.47.38-.85.85-.85z" fill="#primary"/><path d="M22 27h-3v4l-6-4H7a2 2 0 01-2-2V7a2 2 0 012-2h33a2 2 0 012 2v10" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#primary" stroke-linecap="round" stroke-width="2"><path stroke-linejoin="round" d="M17 13l-3 3 3 3M30 13l3 3-3 3"/><path d="M25.03 10.986l-3.015 10.027"/></g></g>', icon:
'<g xmlns="http://www.w3.org/2000/svg" fill="none" fill-rule="evenodd"><path d="M28 37c1.003.005.997-.443 1-1 .004-1.458-.004-4.629 0-6-.007-.564-.068-.987-1-1-2.118 0-4 1.79-4 4s1.882 4 4 4zM48 37c-1.003.005-.997-.443-1-1-.004-1.458.004-4.629 0-6 .007-.564.068-.987 1-1 2.118 0 4 1.79 4 4s-1.882 4-4 4z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M26 51c.798-4.48 4.87-8.082 9.97-8.849.66-.1 1.338-.151 2.028-.151m9.606 4.038c1.27 1.408 2.12 3.102 2.396 4.945M32.002 39.71A8.966 8.966 0 0038 42a8.967 8.967 0 006.02-2.31m-.015-12.394A8.967 8.967 0 0038 25a8.966 8.966 0 00-5.994 2.286" stroke="#secondary" stroke-width="2" stroke-linecap="round"/><path d="M38 45c.17 0 .34-.004.509-.01 5.23-.219 9.596-3.785 11.01-8.613m-.004-6.766C48.052 24.634 43.45 21 38 21c-5.485 0-10.11 3.68-11.542 8.706" stroke="#primary" stroke-width="2"/><path d="M37.85 44h1.173c.54 0 .977.438.977.977v.523a1.5 1.5 0 01-3 0v-.65c0-.47.38-.85.85-.85z" fill="#primary"/><path d="M22 27h-3v4l-6-4H7a2 2 0 01-2-2V7a2 2 0 012-2h33a2 2 0 012 2v10" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><g stroke="#primary" stroke-linecap="round" stroke-width="2"><path stroke-linejoin="round" d="M17 13l-3 3 3 3M30 13l3 3-3 3"/><path d="M25.03 10.986l-3.015 10.027"/></g></g>',
...rank(value, [1, 20, 50, 100, 250]), ...rank(value, [1, 20, 50, 100, 250]),
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -298,13 +317,14 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = null const unlock = null
list.push({ list.push({
title:"Verified", title: "Verified",
text:"Registered a GPG key to sign commits", text: "Registered a GPG key to sign commits",
icon:'<g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="none" fill-rule="evenodd"><path d="M46 17.036v13.016c0 4.014-.587 8.94-4.751 13.67-5.787 5.911-12.816 8.279-13.243 8.283-.426.003-7.91-2.639-13.222-8.283C10.718 39.4 10 34.056 10 30.052V17.036a2 2 0 012-2h32a2 2 0 012 2zM16 15c0-6.616 5.384-12 12-12s12 5.384 12 12" stroke="#secondary"/><path d="M21 15c0-3.744 3.141-7 7-7 3.86 0 7 3.256 7 7m4.703 29.63l-3.672-3.647m-17.99-17.869l-7.127-7.081" stroke="#secondary"/><path d="M28 23a8 8 0 110 16 8 8 0 010-16z" stroke="#primary"/><path stroke="#primary" d="M30.966 29.005l-4 3.994-2.002-1.995"/></g>', icon:
rank:value ? "$" : "X", '<g stroke-linecap="round" stroke-linejoin="round" stroke-width="2" fill="none" fill-rule="evenodd"><path d="M46 17.036v13.016c0 4.014-.587 8.94-4.751 13.67-5.787 5.911-12.816 8.279-13.243 8.283-.426.003-7.91-2.639-13.222-8.283C10.718 39.4 10 34.056 10 30.052V17.036a2 2 0 012-2h32a2 2 0 012 2zM16 15c0-6.616 5.384-12 12-12s12 5.384 12 12" stroke="#secondary"/><path d="M21 15c0-3.744 3.141-7 7-7 3.86 0 7 3.256 7 7m4.703 29.63l-3.672-3.647m-17.99-17.869l-7.127-7.081" stroke="#secondary"/><path d="M28 23a8 8 0 110 16 8 8 0 010-16z" stroke="#primary"/><path stroke="#primary" d="M30.966 29.005l-4 3.994-2.002-1.995"/></g>',
progress:value ? 1 : 0, rank: value ? "$" : "X",
progress: value ? 1 : 0,
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -314,13 +334,14 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = null const unlock = null
list.push({ list.push({
title:"Explorer", title: "Explorer",
text:"Starred a topic on GitHub Explore", text: "Starred a topic on GitHub Explore",
icon:'<g transform="translate(3 4)" fill="none" fill-rule="evenodd"><path d="M10 37.5l.049.073a2 2 0 002.506.705l24.391-11.324a2 2 0 00.854-2.874l-2.668-4.27a2 2 0 00-2.865-.562L10.463 34.947A1.869 1.869 0 0010 37.5zM33.028 28.592l-4.033-6.58" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linejoin="round" d="M15.52 37.004l-2.499-3.979"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M25.008 48.011l.013-15.002M17.984 47.038l6.996-14.035M32.005 47.029l-6.987-14.016"/><path d="M2.032 17.015A23.999 23.999 0 001 24c0 9.3 5.29 17.365 13.025 21.35m22-.027C43.734 41.33 49 33.28 49 24a24 24 0 00-1.025-6.96M34.022 1.754A23.932 23.932 0 0025 0c-2.429 0-4.774.36-6.983 1.032" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M40.64 8.472c-1.102-2.224-.935-4.764 1.382-6.465-.922-.087-2.209.326-3.004.784a6.024 6.024 0 00-2.674 7.229c.94 2.618 3.982 4.864 7.66 3.64 1.292-.429 2.615-1.508 2.996-2.665-1.8.625-5.258-.3-6.36-2.523zM21.013 6.015c-.22-.802-3.018-1.295-4.998-.919M4.998 8.006C2.25 9.22.808 11.146 1.011 12.009" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle stroke="#secondary" stroke-width="2" cx="11" cy="9" r="6"/><path d="M.994 12.022c.351 1.38 5.069 1.25 10.713-.355 5.644-1.603 9.654-4.273 9.303-5.653" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M26.978 10.105c.318 1.123.573 1.373 1.71 1.679-1.135.314-1.388.566-1.698 1.69-.318-1.122-.573-1.373-1.711-1.679 1.135-.314 1.39-.566 1.7-1.69" fill="#secondary"/><path d="M26.978 10.105c.318 1.123.573 1.373 1.71 1.679-1.135.314-1.388.566-1.698 1.69-.318-1.122-.573-1.373-1.711-1.679 1.135-.314 1.39-.566 1.7-1.69z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.929 22.737c.317 1.121.573 1.372 1.71 1.678-1.135.314-1.389.566-1.699 1.69-.318-1.121-.573-1.372-1.71-1.679 1.134-.313 1.389-.566 1.699-1.69" fill="#secondary"/><path d="M9.929 22.737c.317 1.121.573 1.372 1.71 1.678-1.135.314-1.389.566-1.699 1.69-.318-1.121-.573-1.372-1.71-1.679 1.134-.313 1.389-.566 1.699-1.69z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M38.912 33.684c.318 1.122.573 1.373 1.711 1.679-1.136.313-1.39.565-1.7 1.69-.317-1.123-.573-1.372-1.71-1.68 1.136-.313 1.389-.565 1.7-1.689" fill="#secondary"/><path d="M38.912 33.684c.318 1.122.573 1.373 1.711 1.679-1.136.313-1.39.565-1.7 1.69-.317-1.123-.573-1.372-1.71-1.68 1.136-.313 1.389-.565 1.7-1.689z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g>', icon:
rank:value ? "$" : "X", '<g transform="translate(3 4)" fill="none" fill-rule="evenodd"><path d="M10 37.5l.049.073a2 2 0 002.506.705l24.391-11.324a2 2 0 00.854-2.874l-2.668-4.27a2 2 0 00-2.865-.562L10.463 34.947A1.869 1.869 0 0010 37.5zM33.028 28.592l-4.033-6.58" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path stroke="#primary" stroke-width="2" stroke-linejoin="round" d="M15.52 37.004l-2.499-3.979"/><path stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" d="M25.008 48.011l.013-15.002M17.984 47.038l6.996-14.035M32.005 47.029l-6.987-14.016"/><path d="M2.032 17.015A23.999 23.999 0 001 24c0 9.3 5.29 17.365 13.025 21.35m22-.027C43.734 41.33 49 33.28 49 24a24 24 0 00-1.025-6.96M34.022 1.754A23.932 23.932 0 0025 0c-2.429 0-4.774.36-6.983 1.032" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M40.64 8.472c-1.102-2.224-.935-4.764 1.382-6.465-.922-.087-2.209.326-3.004.784a6.024 6.024 0 00-2.674 7.229c.94 2.618 3.982 4.864 7.66 3.64 1.292-.429 2.615-1.508 2.996-2.665-1.8.625-5.258-.3-6.36-2.523zM21.013 6.015c-.22-.802-3.018-1.295-4.998-.919M4.998 8.006C2.25 9.22.808 11.146 1.011 12.009" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle stroke="#secondary" stroke-width="2" cx="11" cy="9" r="6"/><path d="M.994 12.022c.351 1.38 5.069 1.25 10.713-.355 5.644-1.603 9.654-4.273 9.303-5.653" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M26.978 10.105c.318 1.123.573 1.373 1.71 1.679-1.135.314-1.388.566-1.698 1.69-.318-1.122-.573-1.373-1.711-1.679 1.135-.314 1.39-.566 1.7-1.69" fill="#secondary"/><path d="M26.978 10.105c.318 1.123.573 1.373 1.71 1.679-1.135.314-1.388.566-1.698 1.69-.318-1.122-.573-1.373-1.711-1.679 1.135-.314 1.39-.566 1.7-1.69z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.929 22.737c.317 1.121.573 1.372 1.71 1.678-1.135.314-1.389.566-1.699 1.69-.318-1.121-.573-1.372-1.71-1.679 1.134-.313 1.389-.566 1.699-1.69" fill="#secondary"/><path d="M9.929 22.737c.317 1.121.573 1.372 1.71 1.678-1.135.314-1.389.566-1.699 1.69-.318-1.121-.573-1.372-1.71-1.679 1.134-.313 1.389-.566 1.699-1.69z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M38.912 33.684c.318 1.122.573 1.373 1.711 1.679-1.136.313-1.39.565-1.7 1.69-.317-1.123-.573-1.372-1.71-1.68 1.136-.313 1.389-.565 1.7-1.689" fill="#secondary"/><path d="M38.912 33.684c.318 1.122.573 1.373 1.711 1.679-1.136.313-1.39.565-1.7 1.69-.317-1.123-.573-1.372-1.71-1.68 1.136-.313 1.389-.565 1.7-1.689z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g>',
progress:value ? 1 : 0, rank: value ? "$" : "X",
progress: value ? 1 : 0,
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
@@ -330,45 +351,48 @@ export default async function({list, login, data, computed, imports, graphql, qu
const unlock = null const unlock = null
list.push({ list.push({
title:"Automator", title: "Automator",
text:"Use GitHub Actions to automate profile updates", text: "Use GitHub Actions to automate profile updates",
icon:'<g transform="translate(4 5)" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke-linecap="round" stroke-linejoin="round"><path stroke="#primary" d="M26.478 22l.696 2.087 3.478.696v2.782l-3.478 1.392-.696 1.39 1.392 3.48-1.392 1.39L23 33.827l-1.391.695L20.217 38h-2.782l-1.392-3.478-1.39-.696-3.48 1.391-1.39-1.39 1.39-3.48-.695-1.39L7 27.565v-2.782l3.478-1.392.696-1.391-1.391-3.478 1.39-1.392 3.48 1.392 1.39-.696 1.392-3.478h2.782l1.392 3.478 1.391.696 3.478-1.392 1.392 1.392z"/><path stroke="#secondary" d="M24.779 12.899l-1.475-2.212 1.475-1.475 2.95 1.475 1.474-.738.737-2.934h2.212l.737 2.934 1.475.738 2.95-1.475 1.474 1.475-1.475 2.949.738 1.475 2.949.737v2.212l-2.95.737-.737 1.475 1.475 2.949-1.475 1.475-2.949-1.475"/></g><path stroke="#primary" stroke-linecap="round" d="M5.932 5.546l7.082 6.931"/><path stroke="#secondary" stroke-linecap="round" d="M32.959 31.99l8.728 8.532"/><circle stroke="#secondary" cx="44" cy="43" r="3"/><circle stroke="#primary" cx="13" cy="2" r="2"/><circle stroke="#secondary" cx="35" cy="44" r="2"/><circle stroke="#secondary" cx="3" cy="12" r="2"/><circle stroke="#primary" cx="45" cy="34" r="2"/><path d="M3.832 0a3 3 0 110 6 3 3 0 010-6zM8.04 10.613l2.1-.613M10.334 9.758l1.914-5.669" stroke="#primary" stroke-linecap="round"/><path stroke="#secondary" stroke-linecap="round" d="M40.026 35.91l-2.025.591M35.695 41.965l1.843-5.326"/><path d="M16 2h23.038a6 6 0 016 6v24.033" stroke="#primary" stroke-linecap="round"/><path d="M32.038 44.033H9a6 6 0 01-6-6V14" stroke="#secondary" stroke-linecap="round"/><path d="M17.533 22.154l5.113 3.22a1 1 0 01-.006 1.697l-5.113 3.17a1 1 0 01-1.527-.85V23a1 1 0 011.533-.846zm11.58-7.134v-.504a1 1 0 011.53-.85l3.845 2.397a1 1 0 01-.006 1.701l-3.846 2.358" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/></g>', icon:
rank:value ? "$" : "X", '<g transform="translate(4 5)" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke-linecap="round" stroke-linejoin="round"><path stroke="#primary" d="M26.478 22l.696 2.087 3.478.696v2.782l-3.478 1.392-.696 1.39 1.392 3.48-1.392 1.39L23 33.827l-1.391.695L20.217 38h-2.782l-1.392-3.478-1.39-.696-3.48 1.391-1.39-1.39 1.39-3.48-.695-1.39L7 27.565v-2.782l3.478-1.392.696-1.391-1.391-3.478 1.39-1.392 3.48 1.392 1.39-.696 1.392-3.478h2.782l1.392 3.478 1.391.696 3.478-1.392 1.392 1.392z"/><path stroke="#secondary" d="M24.779 12.899l-1.475-2.212 1.475-1.475 2.95 1.475 1.474-.738.737-2.934h2.212l.737 2.934 1.475.738 2.95-1.475 1.474 1.475-1.475 2.949.738 1.475 2.949.737v2.212l-2.95.737-.737 1.475 1.475 2.949-1.475 1.475-2.949-1.475"/></g><path stroke="#primary" stroke-linecap="round" d="M5.932 5.546l7.082 6.931"/><path stroke="#secondary" stroke-linecap="round" d="M32.959 31.99l8.728 8.532"/><circle stroke="#secondary" cx="44" cy="43" r="3"/><circle stroke="#primary" cx="13" cy="2" r="2"/><circle stroke="#secondary" cx="35" cy="44" r="2"/><circle stroke="#secondary" cx="3" cy="12" r="2"/><circle stroke="#primary" cx="45" cy="34" r="2"/><path d="M3.832 0a3 3 0 110 6 3 3 0 010-6zM8.04 10.613l2.1-.613M10.334 9.758l1.914-5.669" stroke="#primary" stroke-linecap="round"/><path stroke="#secondary" stroke-linecap="round" d="M40.026 35.91l-2.025.591M35.695 41.965l1.843-5.326"/><path d="M16 2h23.038a6 6 0 016 6v24.033" stroke="#primary" stroke-linecap="round"/><path d="M32.038 44.033H9a6 6 0 01-6-6V14" stroke="#secondary" stroke-linecap="round"/><path d="M17.533 22.154l5.113 3.22a1 1 0 01-.006 1.697l-5.113 3.17a1 1 0 01-1.527-.85V23a1 1 0 011.533-.846zm11.58-7.134v-.504a1 1 0 011.53-.85l3.845 2.397a1 1 0 01-.006 1.701l-3.846 2.358" stroke="#primary" stroke-linecap="round" stroke-linejoin="round"/></g>',
progress:value ? 1 : 0, rank: value ? "$" : "X",
progress: value ? 1 : 0,
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
//Infographile //Infographile
{ {
const {repository:{viewerHasStarred:value}, viewer:{login:_login}} = await graphql(queries.achievements.metrics()) const {repository: {viewerHasStarred: value}, viewer: {login: _login}} = await graphql(queries.achievements.metrics())
const unlock = null const unlock = null
list.push({ list.push({
title:"Infographile", title: "Infographile",
text:"Fervent supporter of metrics", text: "Fervent supporter of metrics",
icon:'<g stroke-linejoin="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#secondary" stroke-linecap="round"><path d="M22 31h20M22 36h10"/></g><path d="M44.05 36.013a8 8 0 110 16 8 8 0 010-16z" stroke="#primary" stroke-linecap="round"/><path d="M32 43H7c-1.228 0-2-.84-2-2V7c0-1.16.772-2 2-2h7.075M47 24.04V32" stroke="#secondary" stroke-linecap="round"/><path stroke="#primary" stroke-linecap="round" d="M47.015 42.017l-4 3.994-2.001-1.995"/><path stroke="#secondary" d="M11 31h5v5h-5z"/><path d="M11 14a2 2 0 012-2m28 12a2 2 0 01-2 2h-1m-5 0h-4m-6 0h-4m-5 0h-1a2 2 0 01-2-2m0-4v-2" stroke="#secondary" stroke-linecap="round"/><path d="M18 18V7c0-1.246.649-2 1.73-2h28.54C49.351 5 50 5.754 50 7v11c0 1.246-.649 2-1.73 2H19.73c-1.081 0-1.73-.754-1.73-2z" stroke="#primary" stroke-linecap="round"/><path stroke="#primary" stroke-linecap="round" d="M22 13h4l2-3 3 5 2-2h3.052l2.982-4 3.002 4H46"/></g>', icon:
rank:(value) && (login === _login) ? "$" : "X", '<g stroke-linejoin="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke="#secondary" stroke-linecap="round"><path d="M22 31h20M22 36h10"/></g><path d="M44.05 36.013a8 8 0 110 16 8 8 0 010-16z" stroke="#primary" stroke-linecap="round"/><path d="M32 43H7c-1.228 0-2-.84-2-2V7c0-1.16.772-2 2-2h7.075M47 24.04V32" stroke="#secondary" stroke-linecap="round"/><path stroke="#primary" stroke-linecap="round" d="M47.015 42.017l-4 3.994-2.001-1.995"/><path stroke="#secondary" d="M11 31h5v5h-5z"/><path d="M11 14a2 2 0 012-2m28 12a2 2 0 01-2 2h-1m-5 0h-4m-6 0h-4m-5 0h-1a2 2 0 01-2-2m0-4v-2" stroke="#secondary" stroke-linecap="round"/><path d="M18 18V7c0-1.246.649-2 1.73-2h28.54C49.351 5 50 5.754 50 7v11c0 1.246-.649 2-1.73 2H19.73c-1.081 0-1.73-.754-1.73-2z" stroke="#primary" stroke-linecap="round"/><path stroke="#primary" stroke-linecap="round" d="M22 13h4l2-3 3 5 2-2h3.052l2.982-4 3.002 4H46"/></g>',
progress:(value) && (login === _login) ? 1 : 0, rank: (value) && (login === _login) ? "$" : "X",
progress: (value) && (login === _login) ? 1 : 0,
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
//Octonaut //Octonaut
{ {
const {user:{viewerIsFollowing:value}, viewer:{login:_login}} = await graphql(queries.achievements.octocat()) const {user: {viewerIsFollowing: value}, viewer: {login: _login}} = await graphql(queries.achievements.octocat())
const unlock = null const unlock = null
list.push({ list.push({
title:"Octonaut", title: "Octonaut",
text:"Following octocat", text: "Following octocat",
icon:'<g fill="none" fill-rule="evenodd"><path d="M14.7 8c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689zm26 0c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689zM28.021 5c.318 1.122.574 1.372 1.711 1.678-1.136.314-1.389.566-1.7 1.69-.317-1.121-.572-1.372-1.71-1.679 1.135-.313 1.39-.566 1.7-1.689z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><g transform="translate(4 9)" fill-rule="nonzero"><path d="M14.05 9.195C10.327 7.065 7.46 6 5.453 6 4.92 6 4 6.164 3.5 6.653s-.572.741-.711 1.14c-.734 2.1-1.562 6.317.078 9.286-8.767 25.38 15.513 24.92 21.207 24.92 5.695 0 29.746.456 21.037-24.908 1.112-2.2 1.404-5.119.121-9.284-.863-2.802-4.646-2.341-11.35 1.384a27.38 27.38 0 00-9.802-1.81c-3.358 0-6.701.605-10.03 1.814z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M10.323 40.074c-2.442-1.02-2.93-3.308-2.93-4.834 0-1.527.488-2.45.976-3.92.489-1.47.391-2.281-.976-5.711-1.368-3.43.976-7.535 4.884-7.535 3.908 0 7.088 3.005 11.723 2.956m0 0c4.635.05 7.815-2.956 11.723-2.956 3.908 0 6.252 4.105 4.884 7.535-1.367 3.43-1.465 4.241-.976 5.71.488 1.47.976 2.394.976 3.92 0 1.527-.488 3.816-2.93 4.835" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle fill="#primary" cx="12" cy="30" r="1"/><circle fill="#primary" cx="13" cy="28" r="1"/><circle fill="#primary" cx="15" cy="28" r="1"/><circle fill="#primary" cx="23" cy="35" r="1"/><circle fill="#primary" cx="25" cy="35" r="1"/><circle fill="#primary" cx="17" cy="28" r="1"/><circle fill="#primary" cx="31" cy="28" r="1"/><circle fill="#primary" cx="33" cy="28" r="1"/><circle fill="#primary" cx="35" cy="28" r="1"/><circle fill="#primary" cx="12" cy="32" r="1"/><circle fill="#primary" cx="19" cy="30" r="1"/><circle fill="#primary" cx="19" cy="32" r="1"/><circle fill="#primary" cx="29" cy="30" r="1"/><circle fill="#primary" cx="29" cy="32" r="1"/><circle fill="#primary" cx="36" cy="30" r="1"/><circle fill="#primary" cx="36" cy="32" r="1"/></g></g>', icon:
rank:(value) && (login === _login) ? "$" : "X", '<g fill="none" fill-rule="evenodd"><path d="M14.7 8c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689zm26 0c.316 1.122.572 1.372 1.71 1.678-1.136.314-1.39.566-1.7 1.69-.317-1.121-.573-1.372-1.71-1.679 1.135-.313 1.389-.566 1.7-1.689zM28.021 5c.318 1.122.574 1.372 1.711 1.678-1.136.314-1.389.566-1.7 1.69-.317-1.121-.572-1.372-1.71-1.679 1.135-.313 1.39-.566 1.7-1.689z" stroke="#primary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><g transform="translate(4 9)" fill-rule="nonzero"><path d="M14.05 9.195C10.327 7.065 7.46 6 5.453 6 4.92 6 4 6.164 3.5 6.653s-.572.741-.711 1.14c-.734 2.1-1.562 6.317.078 9.286-8.767 25.38 15.513 24.92 21.207 24.92 5.695 0 29.746.456 21.037-24.908 1.112-2.2 1.404-5.119.121-9.284-.863-2.802-4.646-2.341-11.35 1.384a27.38 27.38 0 00-9.802-1.81c-3.358 0-6.701.605-10.03 1.814z" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="M10.323 40.074c-2.442-1.02-2.93-3.308-2.93-4.834 0-1.527.488-2.45.976-3.92.489-1.47.391-2.281-.976-5.711-1.368-3.43.976-7.535 4.884-7.535 3.908 0 7.088 3.005 11.723 2.956m0 0c4.635.05 7.815-2.956 11.723-2.956 3.908 0 6.252 4.105 4.884 7.535-1.367 3.43-1.465 4.241-.976 5.71.488 1.47.976 2.394.976 3.92 0 1.527-.488 3.816-2.93 4.835" stroke="#secondary" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle fill="#primary" cx="12" cy="30" r="1"/><circle fill="#primary" cx="13" cy="28" r="1"/><circle fill="#primary" cx="15" cy="28" r="1"/><circle fill="#primary" cx="23" cy="35" r="1"/><circle fill="#primary" cx="25" cy="35" r="1"/><circle fill="#primary" cx="17" cy="28" r="1"/><circle fill="#primary" cx="31" cy="28" r="1"/><circle fill="#primary" cx="33" cy="28" r="1"/><circle fill="#primary" cx="35" cy="28" r="1"/><circle fill="#primary" cx="12" cy="32" r="1"/><circle fill="#primary" cx="19" cy="30" r="1"/><circle fill="#primary" cx="19" cy="32" r="1"/><circle fill="#primary" cx="29" cy="30" r="1"/><circle fill="#primary" cx="29" cy="32" r="1"/><circle fill="#primary" cx="36" cy="30" r="1"/><circle fill="#primary" cx="36" cy="32" r="1"/></g></g>',
progress:(value) && (login === _login) ? 1 : 0, rank: (value) && (login === _login) ? "$" : "X",
progress: (value) && (login === _login) ? 1 : 0,
value, value,
unlock:new Date(unlock?.createdAt), unlock: new Date(unlock?.createdAt),
}) })
} }
} }

View File

@@ -7,11 +7,11 @@ export default async function({login, data, rest, q, account, imports}, {enabled
return null return null
//Context //Context
let context = {mode:"user"} let context = {mode: "user"}
if (q.repo) { if (q.repo) {
console.debug(`metrics/compute/${login}/plugins > activity > switched to repository mode`) console.debug(`metrics/compute/${login}/plugins > activity > switched to repository mode`)
const {owner, repo} = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})).shift() const {owner, repo} = data.user.repositories.nodes.map(({name: repo, owner: {login: owner}}) => ({repo, owner})).shift()
context = {...context, mode:"repository", owner, repo} context = {...context, mode: "repository", owner, repo}
} }
//Load inputs //Load inputs
@@ -29,7 +29,7 @@ export default async function({login, data, rest, q, account, imports}, {enabled
try { try {
for (let page = 1; page <= pages; page++) { for (let page = 1; page <= pages; page++) {
console.debug(`metrics/compute/${login}/plugins > activity > loading page ${page}/${pages}`) console.debug(`metrics/compute/${login}/plugins > activity > loading page ${page}/${pages}`)
events.push(...(context.mode === "repository" ? await rest.activity.listRepoEvents({owner:context.owner, repo:context.repo}) : await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data) events.push(...(context.mode === "repository" ? await rest.activity.listRepoEvents({owner: context.owner, repo: context.repo}) : await rest.activity.listEventsForAuthenticatedUser({username: login, per_page: 100, page})).data)
} }
} }
catch { catch {
@@ -43,7 +43,7 @@ export default async function({login, data, rest, q, account, imports}, {enabled
.filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase()) .filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase())
.filter(({created_at}) => Number.isFinite(days) ? new Date(created_at) > new Date(Date.now() - days * 24 * 60 * 60 * 1000) : true) .filter(({created_at}) => Number.isFinite(days) ? new Date(created_at) > new Date(Date.now() - days * 24 * 60 * 60 * 1000) : true)
.filter(event => visibility === "public" ? event.public : true) .filter(event => visibility === "public" ? event.public : true)
.map(async ({type, payload, actor:{login:actor}, repo:{name:repo}, created_at}) => { .map(async ({type, payload, actor: {login: actor}, repo: {name: repo}, created_at}) => {
//See https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types //See https://docs.github.com/en/free-pro-team@latest/developers/webhooks-and-events/github-event-types
const timestamp = new Date(created_at) const timestamp = new Date(created_at)
if ((skipped.includes(repo.split("/").pop())) || (skipped.includes(repo))) if ((skipped.includes(repo.split("/").pop())) || (skipped.includes(repo)))
@@ -53,110 +53,110 @@ export default async function({login, data, rest, q, account, imports}, {enabled
case "CommitCommentEvent": { case "CommitCommentEvent": {
if (!["created"].includes(payload.action)) if (!["created"].includes(payload.action))
return null return null
const {comment:{user:{login:user}, commit_id:sha, body:content}} = payload const {comment: {user: {login: user}, commit_id: sha, body: content}} = payload
if (ignored.includes(user)) if (ignored.includes(user))
return null return null
return {type:"comment", on:"commit", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile:null, number:sha.substring(0, 7), title:""} return {type: "comment", on: "commit", actor, timestamp, repo, content: await imports.markdown(content, {mode: markdown, codelines}), user, mobile: null, number: sha.substring(0, 7), title: ""}
} }
//Created a git branch or tag //Created a git branch or tag
case "CreateEvent": { case "CreateEvent": {
const {ref:name, ref_type:type} = payload const {ref: name, ref_type: type} = payload
return {type:"ref/create", actor, timestamp, repo, ref:{name, type}} return {type: "ref/create", actor, timestamp, repo, ref: {name, type}}
} }
//Deleted a git branch or tag //Deleted a git branch or tag
case "DeleteEvent": { case "DeleteEvent": {
const {ref:name, ref_type:type} = payload const {ref: name, ref_type: type} = payload
return {type:"ref/delete", actor, timestamp, repo, ref:{name, type}} return {type: "ref/delete", actor, timestamp, repo, ref: {name, type}}
} }
//Forked repository //Forked repository
case "ForkEvent": { case "ForkEvent": {
const {forkee:{full_name:forked}} = payload const {forkee: {full_name: forked}} = payload
return {type:"fork", actor, timestamp, repo, forked} return {type: "fork", actor, timestamp, repo, forked}
} }
//Wiki editions //Wiki editions
case "GollumEvent": { case "GollumEvent": {
const {pages} = payload const {pages} = payload
return {type:"wiki", actor, timestamp, repo, pages:pages.map(({title}) => title)} return {type: "wiki", actor, timestamp, repo, pages: pages.map(({title}) => title)}
} }
//Commented on an issue //Commented on an issue
case "IssueCommentEvent": { case "IssueCommentEvent": {
if (!["created"].includes(payload.action)) if (!["created"].includes(payload.action))
return null return null
const {issue:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload const {issue: {user: {login: user}, title, number}, comment: {body: content, performed_via_github_app: mobile}} = payload
if (ignored.includes(user)) if (ignored.includes(user))
return null return null
return {type:"comment", on:"issue", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile, number, title} return {type: "comment", on: "issue", actor, timestamp, repo, content: await imports.markdown(content, {mode: markdown, codelines}), user, mobile, number, title}
} }
//Issue event //Issue event
case "IssuesEvent": { case "IssuesEvent": {
if (!["opened", "closed", "reopened"].includes(payload.action)) if (!["opened", "closed", "reopened"].includes(payload.action))
return null return null
const {action, issue:{user:{login:user}, title, number, body:content}} = payload const {action, issue: {user: {login: user}, title, number, body: content}} = payload
if (ignored.includes(user)) if (ignored.includes(user))
return null return null
return {type:"issue", actor, timestamp, repo, action, user, number, title, content:await imports.markdown(content, {mode:markdown, codelines})} return {type: "issue", actor, timestamp, repo, action, user, number, title, content: await imports.markdown(content, {mode: markdown, codelines})}
} }
//Activity from repository collaborators //Activity from repository collaborators
case "MemberEvent": { case "MemberEvent": {
if (!["added"].includes(payload.action)) if (!["added"].includes(payload.action))
return null return null
const {member:{login:user}} = payload const {member: {login: user}} = payload
if (ignored.includes(user)) if (ignored.includes(user))
return null return null
return {type:"member", actor, timestamp, repo, user} return {type: "member", actor, timestamp, repo, user}
} }
//Made repository public //Made repository public
case "PublicEvent": { case "PublicEvent": {
return {type:"public", actor, timestamp, repo} return {type: "public", actor, timestamp, repo}
} }
//Pull requests events //Pull requests events
case "PullRequestEvent": { case "PullRequestEvent": {
if (!["opened", "closed"].includes(payload.action)) if (!["opened", "closed"].includes(payload.action))
return null return null
const {action, pull_request:{user:{login:user}, title, number, body:content, additions:added, deletions:deleted, changed_files:changed, merged}} = payload const {action, pull_request: {user: {login: user}, title, number, body: content, additions: added, deletions: deleted, changed_files: changed, merged}} = payload
if (ignored.includes(user)) if (ignored.includes(user))
return null return null
return {type:"pr", actor, timestamp, repo, action:(action === "closed") && (merged) ? "merged" : action, user, title, number, content:await imports.markdown(content, {mode:markdown, codelines}), lines:{added, deleted}, files:{changed}} return {type: "pr", actor, timestamp, repo, action: (action === "closed") && (merged) ? "merged" : action, user, title, number, content: await imports.markdown(content, {mode: markdown, codelines}), lines: {added, deleted}, files: {changed}}
} }
//Reviewed a pull request //Reviewed a pull request
case "PullRequestReviewEvent": { case "PullRequestReviewEvent": {
const {review:{state:review}, pull_request:{user:{login:user}, number, title}} = payload const {review: {state: review}, pull_request: {user: {login: user}, number, title}} = payload
if (ignored.includes(user)) if (ignored.includes(user))
return null return null
return {type:"review", actor, timestamp, repo, review, user, number, title} return {type: "review", actor, timestamp, repo, review, user, number, title}
} }
//Commented on a pull request //Commented on a pull request
case "PullRequestReviewCommentEvent": { case "PullRequestReviewCommentEvent": {
if (!["created"].includes(payload.action)) if (!["created"].includes(payload.action))
return null return null
const {pull_request:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload const {pull_request: {user: {login: user}, title, number}, comment: {body: content, performed_via_github_app: mobile}} = payload
if (ignored.includes(user)) if (ignored.includes(user))
return null return null
return {type:"comment", on:"pr", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile, number, title} return {type: "comment", on: "pr", actor, timestamp, repo, content: await imports.markdown(content, {mode: markdown, codelines}), user, mobile, number, title}
} }
//Pushed commits //Pushed commits
case "PushEvent": { case "PushEvent": {
let {size, commits, ref} = payload let {size, commits, ref} = payload
commits = commits.filter(({author:{email}}) => !ignored.includes(email)) commits = commits.filter(({author: {email}}) => !ignored.includes(email))
if (!commits.length) if (!commits.length)
return null return null
if (commits.slice(-1).pop()?.message.startsWith("Merge branch ")) if (commits.slice(-1).pop()?.message.startsWith("Merge branch "))
commits = commits.slice(-1) commits = commits.slice(-1)
return {type:"push", actor, timestamp, repo, size, branch:ref.match(/refs.heads.(?<branch>.*)/)?.groups?.branch ?? null, commits:commits.reverse().map(({sha, message}) => ({sha:sha.substring(0, 7), message}))} return {type: "push", actor, timestamp, repo, size, branch: ref.match(/refs.heads.(?<branch>.*)/)?.groups?.branch ?? null, commits: commits.reverse().map(({sha, message}) => ({sha: sha.substring(0, 7), message}))}
} }
//Released //Released
case "ReleaseEvent": { case "ReleaseEvent": {
if (!["published"].includes(payload.action)) if (!["published"].includes(payload.action))
return null return null
const {action, release:{name, tag_name, prerelease, draft, body:content}} = payload const {action, release: {name, tag_name, prerelease, draft, body: content}} = payload
return {type:"release", actor, timestamp, repo, action, name:name || tag_name, prerelease, draft, content:await imports.markdown(content, {mode:markdown, codelines})} return {type: "release", actor, timestamp, repo, action, name: name || tag_name, prerelease, draft, content: await imports.markdown(content, {mode: markdown, codelines})}
} }
//Starred a repository //Starred a repository
case "WatchEvent": { case "WatchEvent": {
if (!["started"].includes(payload.action)) if (!["started"].includes(payload.action))
return null return null
const {action} = payload const {action} = payload
return {type:"star", actor, timestamp, repo, action} return {type: "star", actor, timestamp, repo, action}
} }
//Unknown event //Unknown event
default: { default: {
@@ -170,10 +170,10 @@ export default async function({login, data, rest, q, account, imports}, {enabled
.slice(0, limit) .slice(0, limit)
//Results //Results
return {timestamps, events:activity} return {timestamps, events: activity}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -7,17 +7,17 @@ export default async function({login, data, queries, imports, q, account}, {enab
return null return null
//Load inputs //Load inputs
let {limit, "limit.characters":limit_characters, medias, sections, shuffle, user} = imports.metadata.plugins.anilist.inputs({data, account, q}) let {limit, "limit.characters": limit_characters, medias, sections, shuffle, user} = imports.metadata.plugins.anilist.inputs({data, account, q})
//Initialization //Initialization
const result = {user:{name:user, stats:null, genres:[]}, lists:Object.fromEntries(medias.map(type => [type, {}])), characters:[], sections} const result = {user: {name: user, stats: null, genres: []}, lists: Object.fromEntries(medias.map(type => [type, {}])), characters: [], sections}
//User statistics //User statistics
for (let retried = false; !retried; retried = true) { for (let retried = false; !retried; retried = true) {
try { try {
//Query API //Query API
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (user statistics)`) console.debug(`metrics/compute/${login}/plugins > anilist > querying api (user statistics)`)
const {data:{data:{User:{statistics:stats}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user}, query:queries.anilist.statistics()}) const {data: {data: {User: {statistics: stats}}}} = await imports.axios.post("https://graphql.anilist.co", {variables: {name: user}, query: queries.anilist.statistics()})
//Format and save results //Format and save results
result.user.stats = stats result.user.stats = stats
result.user.genres = [...new Set([...stats.anime.genres.map(({genre}) => genre), ...stats.manga.genres.map(({genre}) => genre)])] result.user.genres = [...new Set([...stats.anime.genres.map(({genre}) => genre), ...stats.manga.genres.map(({genre}) => genre)])]
@@ -34,7 +34,7 @@ export default async function({login, data, queries, imports, q, account}, {enab
try { try {
//Query API //Query API
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (medias lists - ${type})`) console.debug(`metrics/compute/${login}/plugins > anilist > querying api (medias lists - ${type})`)
const {data:{data:{MediaListCollection:{lists}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user, type:type.toLocaleUpperCase()}, query:queries.anilist.medias()}) const {data: {data: {MediaListCollection: {lists}}}} = await imports.axios.post("https://graphql.anilist.co", {variables: {name: user, type: type.toLocaleUpperCase()}, query: queries.anilist.medias()})
//Format and save results //Format and save results
for (const {name, entries} of lists) { for (const {name, entries} of lists) {
//Format results //Format results
@@ -65,10 +65,10 @@ export default async function({login, data, queries, imports, q, account}, {enab
do { do {
try { try {
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (favorites ${type}s - page ${page})`) console.debug(`metrics/compute/${login}/plugins > anilist > querying api (favorites ${type}s - page ${page})`)
const {data:{data:{User:{favourites:{[type]:{nodes, pageInfo:cursor}}}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user, page}, query:queries.anilist.favorites({type})}) const {data: {data: {User: {favourites: {[type]: {nodes, pageInfo: cursor}}}}}} = await imports.axios.post("https://graphql.anilist.co", {variables: {name: user, page}, query: queries.anilist.favorites({type})})
page++ page++
next = cursor.hasNextPage next = cursor.hasNextPage
list.push(...await Promise.all(nodes.map(media => format({media:{progess:null, score:null, media}, imports})))) list.push(...await Promise.all(nodes.map(media => format({media: {progess: null, score: null, media}, imports}))))
} }
catch (error) { catch (error) {
if (await retry({login, imports, error})) if (await retry({login, imports, error}))
@@ -95,12 +95,12 @@ export default async function({login, data, queries, imports, q, account}, {enab
do { do {
try { try {
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (favorites characters - page ${page})`) console.debug(`metrics/compute/${login}/plugins > anilist > querying api (favorites characters - page ${page})`)
const {data:{data:{User:{favourites:{characters:{nodes, pageInfo:cursor}}}}}} = await imports.axios.post("https://graphql.anilist.co", {variables:{name:user, page}, query:queries.anilist.characters()}) const {data: {data: {User: {favourites: {characters: {nodes, pageInfo: cursor}}}}}} = await imports.axios.post("https://graphql.anilist.co", {variables: {name: user, page}, query: queries.anilist.characters()})
page++ page++
next = cursor.hasNextPage next = cursor.hasNextPage
for (const {name:{full:name}, image:{medium:artwork}} of nodes) { for (const {name: {full: name}, image: {medium: artwork}} of nodes) {
console.debug(`metrics/compute/${login}/plugins > anilist > processing ${name}`) console.debug(`metrics/compute/${login}/plugins > anilist > processing ${name}`)
characters.push({name, artwork:await imports.imgb64(artwork)}) characters.push({name, artwork: await imports.imgb64(artwork)})
} }
} }
catch (error) { catch (error) {
@@ -129,24 +129,24 @@ export default async function({login, data, queries, imports, q, account}, {enab
message = `API returned ${status}` message = `API returned ${status}`
error = error.response?.data ?? null error = error.response?.data ?? null
} }
throw {error:{message, instance:error}} throw {error: {message, instance: error}}
} }
} }
/**Media formatter */ /**Media formatter */
async function format({media, imports}) { async function format({media, imports}) {
const {progress, score:userScore, media:{title, description, status, startDate:{year:release}, genres, averageScore, episodes, chapters, type, coverImage:{medium:artwork}}} = media const {progress, score: userScore, media: {title, description, status, startDate: {year: release}, genres, averageScore, episodes, chapters, type, coverImage: {medium: artwork}}} = media
return { return {
name:title.romaji, name: title.romaji,
type, type,
status, status,
release, release,
genres, genres,
progress, progress,
description:description.replace(/<br\s*\\?>/g, " "), description: description.replace(/<br\s*\\?>/g, " "),
scores:{user:userScore, community:averageScore}, scores: {user: userScore, community: averageScore},
released:type === "ANIME" ? episodes : chapters, released: type === "ANIME" ? episodes : chapters,
artwork:await imports.imgb64(artwork), artwork: await imports.imgb64(artwork),
} }
} }

View File

@@ -7,7 +7,7 @@
export default async function({login, graphql, rest, data, q, queries, imports}, conf) { export default async function({login, graphql, rest, data, q, queries, imports}, conf) {
//Load inputs //Load inputs
console.debug(`metrics/compute/${login}/base > started`) console.debug(`metrics/compute/${login}/base > started`)
let {indepth, "repositories.forks":_forks, "repositories.affiliations":_affiliations, "repositories.batch":_batch} = imports.metadata.plugins.base.inputs({data, q, account:"bypass"}) let {indepth, "repositories.forks": _forks, "repositories.affiliations": _affiliations, "repositories.batch": _batch} = imports.metadata.plugins.base.inputs({data, q, account: "bypass"})
const extras = conf.settings.extras?.features ?? conf.settings.extras?.default const extras = conf.settings.extras?.features ?? conf.settings.extras?.default
const repositories = conf.settings.repositories || 100 const repositories = conf.settings.repositories || 100
const forks = _forks ? "" : ", isFork: false" const forks = _forks ? "" : ", isFork: false"
@@ -29,18 +29,18 @@ export default async function({login, graphql, rest, data, q, queries, imports},
//Query data from GitHub API //Query data from GitHub API
console.debug(`metrics/compute/${login}/base > account ${account}`) console.debug(`metrics/compute/${login}/base > account ${account}`)
const queried = await graphql(queries.base[account]({login})) const queried = await graphql(queries.base[account]({login}))
Object.assign(data, {user:queried[account]}) Object.assign(data, {user: queried[account]})
postprocess?.[account]({login, data}) postprocess?.[account]({login, data})
try { try {
Object.assign(data.user, (await graphql(queries.base[`${account}.x`]({login, account, "calendar.from":new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), "calendar.to":(new Date()).toISOString()})))[account]) Object.assign(data.user, (await graphql(queries.base[`${account}.x`]({login, account, "calendar.from": new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), "calendar.to": (new Date()).toISOString()})))[account])
console.debug(`metrics/compute/${login}/base > successfully loaded bulk query`) console.debug(`metrics/compute/${login}/base > successfully loaded bulk query`)
} }
catch { catch {
console.debug(`metrics/compute/${login}/base > failed to load bulk query, falling back to unit queries`) console.debug(`metrics/compute/${login}/base > failed to load bulk query, falling back to unit queries`)
//Query basic fields //Query basic fields
const fields = { const fields = {
user:["packages", "starredRepositories", "watching", "sponsorshipsAsSponsor", "sponsorshipsAsMaintainer", "followers", "following", "issueComments", "organizations", "repositoriesContributedTo(includeUserRepositories: true)"], user: ["packages", "starredRepositories", "watching", "sponsorshipsAsSponsor", "sponsorshipsAsMaintainer", "followers", "following", "issueComments", "organizations", "repositoriesContributedTo(includeUserRepositories: true)"],
organization:["packages", "sponsorshipsAsSponsor", "sponsorshipsAsMaintainer", "membersWithRole"], organization: ["packages", "sponsorshipsAsSponsor", "sponsorshipsAsMaintainer", "membersWithRole"],
}[account] ?? [] }[account] ?? []
for (const field of fields) { for (const field of fields) {
try { try {
@@ -48,7 +48,7 @@ export default async function({login, graphql, rest, data, q, queries, imports},
} }
catch { catch {
console.debug(`metrics/compute/${login}/base > failed to retrieve ${field}`) console.debug(`metrics/compute/${login}/base > failed to retrieve ${field}`)
data.user[field] = {totalCount:NaN} data.user[field] = {totalCount: NaN}
} }
} }
//Query repositories fields //Query repositories fields
@@ -68,7 +68,7 @@ export default async function({login, graphql, rest, data, q, queries, imports},
const fields = ["totalRepositoriesWithContributedCommits", "totalCommitContributions", "restrictedContributionsCount", "totalIssueContributions", "totalPullRequestContributions", "totalPullRequestReviewContributions"] const fields = ["totalRepositoriesWithContributedCommits", "totalCommitContributions", "restrictedContributionsCount", "totalIssueContributions", "totalPullRequestContributions", "totalPullRequestReviewContributions"]
for (const field of fields) { for (const field of fields) {
try { try {
Object.assign(data.user.contributionsCollection, (await graphql(queries.base.contributions({login, account, field, range:""})))[account].contributionsCollection) Object.assign(data.user.contributionsCollection, (await graphql(queries.base.contributions({login, account, field, range: ""})))[account].contributionsCollection)
} }
catch { catch {
console.debug(`metrics/compute/${login}/base > failed to retrieve contributionsCollection.${field}`) console.debug(`metrics/compute/${login}/base > failed to retrieve contributionsCollection.${field}`)
@@ -78,17 +78,17 @@ export default async function({login, graphql, rest, data, q, queries, imports},
} }
//Query calendar //Query calendar
try { try {
Object.assign(data.user, (await graphql(queries.base.calendar({login, "calendar.from":new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), "calendar.to":(new Date()).toISOString()})))[account]) Object.assign(data.user, (await graphql(queries.base.calendar({login, "calendar.from": new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), "calendar.to": (new Date()).toISOString()})))[account])
} }
catch { catch {
console.debug(`metrics/compute/${login}/base > failed to retrieve contributions calendar`) console.debug(`metrics/compute/${login}/base > failed to retrieve contributions calendar`)
data.user.calendar = {contributionCalendar:{weeks:[]}} data.user.calendar = {contributionCalendar: {weeks: []}}
} }
} }
} }
//Query contributions collection over account lifetime instead of last year //Query contributions collection over account lifetime instead of last year
if (account === "user") { if (account === "user") {
if ((indepth)&&(extras)) { if ((indepth) && (extras)) {
const fields = ["totalRepositoriesWithContributedCommits", "totalCommitContributions", "restrictedContributionsCount", "totalIssueContributions", "totalPullRequestContributions", "totalPullRequestReviewContributions"] const fields = ["totalRepositoriesWithContributedCommits", "totalCommitContributions", "restrictedContributionsCount", "totalIssueContributions", "totalPullRequestContributions", "totalPullRequestReviewContributions"]
const start = new Date(data.user.createdAt) const start = new Date(data.user.createdAt)
const end = new Date() const end = new Date()
@@ -111,7 +111,7 @@ export default async function({login, graphql, rest, data, q, queries, imports},
//Fetch data from api //Fetch data from api
try { try {
console.debug(`metrics/compute/${login}/plugins > base > loading contributions collections for ${field} from "${from.toISOString()}" to "${dto.toISOString()}"`) console.debug(`metrics/compute/${login}/plugins > base > loading contributions collections for ${field} from "${from.toISOString()}" to "${dto.toISOString()}"`)
const {[account]:{contributionsCollection}} = await graphql(queries.base.contributions({login, account, field, range:`(from: "${from.toISOString()}", to: "${dto.toISOString()}")`})) const {[account]: {contributionsCollection}} = await graphql(queries.base.contributions({login, account, field, range: `(from: "${from.toISOString()}", to: "${dto.toISOString()}")`}))
collection[field] += contributionsCollection[field] collection[field] += contributionsCollection[field]
} }
catch { catch {
@@ -127,7 +127,7 @@ export default async function({login, graphql, rest, data, q, queries, imports},
else { else {
try { try {
console.debug(`metrics/compute/${login}/base > loading user commits history`) console.debug(`metrics/compute/${login}/base > loading user commits history`)
const {data:{total_count:total = 0}} = await rest.search.commits({q:`author:${login}`}) const {data: {total_count: total = 0}} = await rest.search.commits({q: `author:${login}`})
data.user.contributionsCollection.totalCommitContributions = Math.max(total, data.user.contributionsCollection.totalCommitContributions) data.user.contributionsCollection.totalCommitContributions = Math.max(total, data.user.contributionsCollection.totalCommitContributions)
} }
catch { catch {
@@ -136,18 +136,18 @@ export default async function({login, graphql, rest, data, q, queries, imports},
} }
} }
//Query repositories from GitHub API //Query repositories from GitHub API
for (const type of ({user:["repositories", "repositoriesContributedTo"], organization:["repositories"]}[account] ?? [])) { for (const type of ({user: ["repositories", "repositoriesContributedTo"], organization: ["repositories"]}[account] ?? [])) {
//Iterate through repositories //Iterate through repositories
let cursor = null let cursor = null
let pushed = 0 let pushed = 0
const options = {repositories:{forks, affiliations, constraints:""}, repositoriesContributedTo:{forks:"", affiliations:"", constraints:", includeUserRepositories: false, contributionTypes: COMMIT"}}[type] ?? null const options = {repositories: {forks, affiliations, constraints: ""}, repositoriesContributedTo: {forks: "", affiliations: "", constraints: ", includeUserRepositories: false, contributionTypes: COMMIT"}}[type] ?? null
data.user[type] = data.user[type] ?? {} data.user[type] = data.user[type] ?? {}
data.user[type].nodes = data.user[type].nodes ?? [] data.user[type].nodes = data.user[type].nodes ?? []
do { do {
console.debug(`metrics/compute/${login}/base > retrieving ${type} after ${cursor}`) console.debug(`metrics/compute/${login}/base > retrieving ${type} after ${cursor}`)
const request = {} const request = {}
try { try {
Object.assign(request, await graphql(queries.base.repositories({login, account, type, after:cursor ? `after: "${cursor}"` : "", repositories:Math.min(repositories, {user:_batch, organization:Math.min(25, _batch)}[account]), ...options}))) Object.assign(request, await graphql(queries.base.repositories({login, account, type, after: cursor ? `after: "${cursor}"` : "", repositories: Math.min(repositories, {user: _batch, organization: Math.min(25, _batch)}[account]), ...options})))
} }
catch (error) { catch (error) {
console.debug(`metrics/compute/${login}/base > failed to retrieve ${_batch} repositories after ${cursor}, this is probably due to an API timeout, halving batch`) console.debug(`metrics/compute/${login}/base > failed to retrieve ${_batch} repositories after ${cursor}, this is probably due to an API timeout, halving batch`)
@@ -158,7 +158,7 @@ export default async function({login, graphql, rest, data, q, queries, imports},
} }
continue continue
} }
const {[account]:{[type]:{edges = [], nodes = []} = {}}} = request const {[account]: {[type]: {edges = [], nodes = []} = {}}} = request
cursor = edges?.[edges?.length - 1]?.cursor cursor = edges?.[edges?.length - 1]?.cursor
data.user[type].nodes.push(...nodes) data.user[type].nodes.push(...nodes)
pushed = nodes.length pushed = nodes.length
@@ -176,7 +176,7 @@ export default async function({login, graphql, rest, data, q, queries, imports},
//Fetch missing packages count from ghcr.io using REST API (as GraphQL API does not support it yet) //Fetch missing packages count from ghcr.io using REST API (as GraphQL API does not support it yet)
try { try {
console.debug(`metrics/compute/${login}/base > patching packages count if possible`) console.debug(`metrics/compute/${login}/base > patching packages count if possible`)
const {data:packages} = await rest.packages[{user:"listPackagesForUser", organization:"listPackagesForOrganization"}[account]]({package_type:"container", org:login, username:login}) const {data: packages} = await rest.packages[{user: "listPackagesForUser", organization: "listPackagesForOrganization"}[account]]({package_type: "container", org: login, username: login})
data.user.packages.totalCount += packages.length data.user.packages.totalCount += packages.length
console.debug(`metrics/compute/${login}/base > patched packages count (added ${packages.length} from ghcr.io)`) console.debug(`metrics/compute/${login}/base > patched packages count (added ${packages.length} from ghcr.io)`)
} }
@@ -184,8 +184,8 @@ export default async function({login, graphql, rest, data, q, queries, imports},
console.debug(`metrics/compute/${login}/base > failed to patch packages count, maybe read:packages scope was not provided`) console.debug(`metrics/compute/${login}/base > failed to patch packages count, maybe read:packages scope was not provided`)
} }
//Shared options //Shared options
let {"repositories.skipped":skipped, "users.ignored":ignored, "commits.authoring":authoring} = imports.metadata.plugins.base.inputs({data, q, account:"bypass"}) let {"repositories.skipped": skipped, "users.ignored": ignored, "commits.authoring": authoring} = imports.metadata.plugins.base.inputs({data, q, account: "bypass"})
data.shared = {"repositories.skipped":skipped, "users.ignored":ignored, "commits.authoring":authoring, "repositories.batch":_batch} data.shared = {"repositories.skipped": skipped, "users.ignored": ignored, "commits.authoring": authoring, "repositories.batch": _batch}
console.debug(`metrics/compute/${login}/base > shared options > ${JSON.stringify(data.shared)}`) console.debug(`metrics/compute/${login}/base > shared options > ${JSON.stringify(data.shared)}`)
//Success //Success
console.debug(`metrics/compute/${login}/base > graphql query > account ${account} > success`) console.debug(`metrics/compute/${login}/base > graphql query > account ${account} > success`)
@@ -213,9 +213,9 @@ const postprocess = {
console.debug(`metrics/compute/${login}/base > applying postprocessing`) console.debug(`metrics/compute/${login}/base > applying postprocessing`)
data.account = "user" data.account = "user"
Object.assign(data.user, { Object.assign(data.user, {
isVerified:false, isVerified: false,
repositories:{}, repositories: {},
contributionsCollection:{}, contributionsCollection: {},
}) })
}, },
//Organization //Organization
@@ -223,44 +223,44 @@ const postprocess = {
console.debug(`metrics/compute/${login}/base > applying postprocessing`) console.debug(`metrics/compute/${login}/base > applying postprocessing`)
data.account = "organization" data.account = "organization"
Object.assign(data.user, { Object.assign(data.user, {
isHireable:false, isHireable: false,
repositories:{}, repositories: {},
starredRepositories:{totalCount:NaN}, starredRepositories: {totalCount: NaN},
watching:{totalCount:NaN}, watching: {totalCount: NaN},
contributionsCollection:{ contributionsCollection: {
totalRepositoriesWithContributedCommits:NaN, totalRepositoriesWithContributedCommits: NaN,
totalCommitContributions:NaN, totalCommitContributions: NaN,
restrictedContributionsCount:NaN, restrictedContributionsCount: NaN,
totalIssueContributions:NaN, totalIssueContributions: NaN,
totalPullRequestContributions:NaN, totalPullRequestContributions: NaN,
totalPullRequestReviewContributions:NaN, totalPullRequestReviewContributions: NaN,
}, },
calendar:{contributionCalendar:{weeks:[]}}, calendar: {contributionCalendar: {weeks: []}},
repositoriesContributedTo:{totalCount:NaN, nodes:[]}, repositoriesContributedTo: {totalCount: NaN, nodes: []},
followers:{totalCount:NaN}, followers: {totalCount: NaN},
following:{totalCount:NaN}, following: {totalCount: NaN},
issueComments:{totalCount:NaN}, issueComments: {totalCount: NaN},
organizations:{totalCount:NaN}, organizations: {totalCount: NaN},
}) })
}, },
//Skip base content query and instantiate an empty user instance //Skip base content query and instantiate an empty user instance
skip({login, data, imports}) { skip({login, data, imports}) {
data.user = {} data.user = {}
data.shared = imports.metadata.plugins.base.inputs({data, q:{}, account:"bypass"}) data.shared = imports.metadata.plugins.base.inputs({data, q: {}, account: "bypass"})
for (const account of ["user", "organization"]) for (const account of ["user", "organization"])
postprocess?.[account]({login, data}) postprocess?.[account]({login, data})
data.account = "bypass" data.account = "bypass"
Object.assign(data.user, { Object.assign(data.user, {
databaseId:NaN, databaseId: NaN,
name:login, name: login,
login, login,
createdAt:new Date(), createdAt: new Date(),
avatarUrl:`https://github.com/${login}.png`, avatarUrl: `https://github.com/${login}.png`,
websiteUrl:null, websiteUrl: null,
twitterUsername:login, twitterUsername: login,
repositories:{totalCount:NaN, totalDiskUsage:NaN, nodes:[]}, repositories: {totalCount: NaN, totalDiskUsage: NaN, nodes: []},
packages:{totalCount:NaN}, packages: {totalCount: NaN},
repositoriesContributedTo:{totalCount:NaN, nodes:[]}, repositoriesContributedTo: {totalCount: NaN, nodes: []},
}) })
}, },
} }

View File

@@ -7,11 +7,11 @@ export default async function({login, q, imports, data, rest, account}, {enabled
return null return null
//Context //Context
let context = {mode:"user"} let context = {mode: "user"}
if (q.repo) { if (q.repo) {
console.debug(`metrics/compute/${login}/plugins > code > switched to repository mode`) console.debug(`metrics/compute/${login}/plugins > code > switched to repository mode`)
const {owner, repo} = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})).shift() const {owner, repo} = data.user.repositories.nodes.map(({name: repo, owner: {login: owner}}) => ({repo, owner})).shift()
context = {...context, mode:"repository", owner, repo} context = {...context, mode: "repository", owner, repo}
} }
//Load inputs //Load inputs
@@ -31,14 +31,14 @@ export default async function({login, q, imports, data, rest, account}, {enabled
...[ ...[
...await Promise.all([ ...await Promise.all([
...(context.mode === "repository" ...(context.mode === "repository"
? await rest.activity.listRepoEvents({owner:context.owner, repo:context.repo}) ? await rest.activity.listRepoEvents({owner: context.owner, repo: context.repo})
: await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data : await rest.activity.listEventsForAuthenticatedUser({username: login, per_page: 100, page})).data
.filter(({type}) => type === "PushEvent") .filter(({type}) => type === "PushEvent")
.filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase()) .filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase())
.filter(({repo:{name:repo}}) => !((skipped.includes(repo.split("/").pop())) || (skipped.includes(repo)))) .filter(({repo: {name: repo}}) => !((skipped.includes(repo.split("/").pop())) || (skipped.includes(repo))))
.filter(event => visibility === "public" ? event.public : true) .filter(event => visibility === "public" ? event.public : true)
.filter(({created_at}) => Number.isFinite(days) ? new Date(created_at) > new Date(Date.now() - days * 24 * 60 * 60 * 1000) : true) .filter(({created_at}) => Number.isFinite(days) ? new Date(created_at) > new Date(Date.now() - days * 24 * 60 * 60 * 1000) : true)
.flatMap(({created_at:created, payload}) => Promise.all(payload.commits.map(async commit => ({created:new Date(created), ...(await rest.request(commit.url)).data})))), .flatMap(({created_at: created, payload}) => Promise.all(payload.commits.map(async commit => ({created: new Date(created), ...(await rest.request(commit.url)).data})))),
]), ]),
] ]
.flat() .flat()
@@ -54,11 +54,10 @@ export default async function({login, q, imports, data, rest, account}, {enabled
//Search for a random snippet //Search for a random snippet
let files = events let files = events
.flatMap(({created, sha, commit:{message, url}, files}) => files.map(({filename, status, additions, deletions, patch}) => ({created, sha, message, filename, status, additions, deletions, patch, repo:url.match(/repos[/](?<repo>[\s\S]+)[/]git[/]commits/)?.groups?.repo})) .flatMap(({created, sha, commit: {message, url}, files}) => files.map(({filename, status, additions, deletions, patch}) => ({created, sha, message, filename, status, additions, deletions, patch, repo: url.match(/repos[/](?<repo>[\s\S]+)[/]git[/]commits/)?.groups?.repo})))
)
.filter(({patch}) => (patch ? (patch.match(/\n/mg)?.length ?? 1) : Infinity) < lines) .filter(({patch}) => (patch ? (patch.match(/\n/mg)?.length ?? 1) : Infinity) < lines)
for (const file of files) for (const file of files)
file.language = await imports.language({...file, prefix:login}).catch(() => "unknown") file.language = await imports.language({...file, prefix: login}).catch(() => "unknown")
files = files.filter(({language}) => (!languages.length) || (languages.includes(language.toLocaleLowerCase()))) files = files.filter(({language}) => (!languages.length) || (languages.includes(language.toLocaleLowerCase())))
const snippet = files[Math.floor(Math.random() * files.length)] ?? null const snippet = files[Math.floor(Math.random() * files.length)] ?? null
if (snippet) { if (snippet) {
@@ -80,6 +79,6 @@ export default async function({login, q, imports, data, rest, account}, {enabled
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -11,19 +11,19 @@ export default async function({q, data, imports, account}, {enabled = false} = {
//Fortunes list //Fortunes list
const fortunes = [ const fortunes = [
{chance:.06, color:"#F51C6A", text:"Reply hazy"}, {chance: .06, color: "#F51C6A", text: "Reply hazy"},
{chance:.03, color:"#FD4D32", text:"Excellent Luck"}, {chance: .03, color: "#FD4D32", text: "Excellent Luck"},
{chance:.16, color:"#E7890C", text:"Good Luck"}, {chance: .16, color: "#E7890C", text: "Good Luck"},
{chance:.24, color:"#BAC200", text:"Average Luck"}, {chance: .24, color: "#BAC200", text: "Average Luck"},
{chance:.16, color:"#7FEC11", text:"Bad Luck"}, {chance: .16, color: "#7FEC11", text: "Bad Luck"},
{chance:.06, color:"#43FD3B", text:"Good news will come to you by mail"}, {chance: .06, color: "#43FD3B", text: "Good news will come to you by mail"},
{chance:.06, color:"#16F174", text:" ´_ゝ`)フーン "}, {chance: .06, color: "#16F174", text: " ´_ゝ`)フーン "},
{chance:.06, color:"#00CBB0", text:"キタ━━━━━━(゚∀゚)━━━━━━ !!!!"}, {chance: .06, color: "#00CBB0", text: "キタ━━━━━━(゚∀゚)━━━━━━ !!!!"},
{chance:.06, color:"#0893E1", text:"You will meet a dark handsome stranger"}, {chance: .06, color: "#0893E1", text: "You will meet a dark handsome stranger"},
{chance:.06, color:"#2A56FB", text:"Better not tell you now"}, {chance: .06, color: "#2A56FB", text: "Better not tell you now"},
{chance:.06, color:"#6023F8", text:"Outlook good"}, {chance: .06, color: "#6023F8", text: "Outlook good"},
{chance:.04, color:"#9D05DA", text:"Very Bad Luck"}, {chance: .04, color: "#9D05DA", text: "Very Bad Luck"},
{chance:.01, color:"#D302A7", text:"Godly Luck"}, {chance: .01, color: "#D302A7", text: "Godly Luck"},
] ]
//Result //Result
@@ -40,6 +40,6 @@ export default async function({q, data, imports, account}, {enabled = false} = {
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -10,7 +10,7 @@ export default async function({q, imports, data, account}, {enabled = false} = {
let {url, datapoints, lowalert, highalert, urgentlowalert, urgenthighalert} = imports.metadata.plugins.nightscout.inputs({data, account, q}) let {url, datapoints, lowalert, highalert, urgentlowalert, urgenthighalert} = imports.metadata.plugins.nightscout.inputs({data, account, q})
if (!url || url === "https://example.herokuapp.com") if (!url || url === "https://example.herokuapp.com")
throw {error:{message:"Nightscout site URL isn't set!"}} throw {error: {message: "Nightscout site URL isn't set!"}}
if (url.substring(url.length - 1) !== "/") if (url.substring(url.length - 1) !== "/")
url += "/" url += "/"
if (url.substring(0, 7) === "http://") if (url.substring(0, 7) === "http://")
@@ -44,13 +44,13 @@ export default async function({q, imports, data, account}, {enabled = false} = {
resp.data[i].color = color resp.data[i].color = color
resp.data[i].alert = alertName resp.data[i].alert = alertName
} }
return {data:resp.data.reverse()} return {data: resp.data.reverse()}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -7,10 +7,10 @@ export default async function({q, imports, data, account}, {enabled = false, tok
return null return null
if (!token) if (!token)
return {poops:[], days:7} return {poops: [], days: 7}
const {days} = imports.metadata.plugins.poopmap.inputs({data, account, q}) const {days} = imports.metadata.plugins.poopmap.inputs({data, account, q})
const {data:{poops}} = await imports.axios.get(`https://api.poopmap.net/api/v1/public_links/${token}`) const {data: {poops}} = await imports.axios.get(`https://api.poopmap.net/api/v1/public_links/${token}`)
const filteredPoops = poops.filter(poop => { const filteredPoops = poops.filter(poop => {
const createdAt = new Date(poop.created_at) const createdAt = new Date(poop.created_at)
@@ -18,7 +18,7 @@ export default async function({q, imports, data, account}, {enabled = false, tok
return createdAt > new Date().getTime() - days * 24 * 60 * 60 * 1000 return createdAt > new Date().getTime() - days * 24 * 60 * 60 * 1000
}) })
const hours = {max:0} const hours = {max: 0}
for (let i = 0; i < filteredPoops.length; i++) { for (let i = 0; i < filteredPoops.length; i++) {
const poop = filteredPoops[i] const poop = filteredPoops[i]
const hour = new Date(poop.created_at).getHours() const hour = new Date(poop.created_at).getHours()
@@ -28,10 +28,10 @@ export default async function({q, imports, data, account}, {enabled = false, tok
} }
//Results //Results
return {poops:hours, days} return {poops: hours, days}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -9,14 +9,14 @@ export default async function({login, q, imports, data, account}, {enabled = fal
//Load inputs //Load inputs
let {url, selector, title, background} = imports.metadata.plugins.screenshot.inputs({data, account, q}) let {url, selector, title, background} = imports.metadata.plugins.screenshot.inputs({data, account, q})
if (!url) if (!url)
throw {error:{message:"An url is required"}} throw {error: {message: "An url is required"}}
//Start puppeteer and navigate to page //Start puppeteer and navigate to page
console.debug(`metrics/compute/${login}/plugins > screenshot > starting browser`) console.debug(`metrics/compute/${login}/plugins > screenshot > starting browser`)
const browser = await imports.puppeteer.launch() const browser = await imports.puppeteer.launch()
console.debug(`metrics/compute/${login}/plugins > screenshot > started ${await browser.version()}`) console.debug(`metrics/compute/${login}/plugins > screenshot > started ${await browser.version()}`)
const page = await browser.newPage() const page = await browser.newPage()
await page.setViewport({width:1280, height:1280}) await page.setViewport({width: 1280, height: 1280})
console.debug(`metrics/compute/${login}/plugins > screenshot > loading ${url}`) console.debug(`metrics/compute/${login}/plugins > screenshot > loading ${url}`)
await page.goto(url) await page.goto(url)
@@ -27,17 +27,17 @@ export default async function({login, q, imports, data, account}, {enabled = fal
return {x, y, width, height} return {x, y, width, height}
}, selector) }, selector)
console.debug(`metrics/compute/${login}/plugins > screenshot > coordinates ${JSON.stringify(clip)}`) console.debug(`metrics/compute/${login}/plugins > screenshot > coordinates ${JSON.stringify(clip)}`)
const [buffer] = await imports.record({page, ...clip, frames:1, background}) const [buffer] = await imports.record({page, ...clip, frames: 1, background})
const screenshot = await (await imports.jimp.read(Buffer.from(buffer.split(",").pop(), "base64"))).resize(Math.min(454 * (1 + data.large), clip.width), imports.jimp.AUTO) const screenshot = await (await imports.jimp.read(Buffer.from(buffer.split(",").pop(), "base64"))).resize(Math.min(454 * (1 + data.large), clip.width), imports.jimp.AUTO)
await browser.close() await browser.close()
//Results //Results
return {image:await screenshot.getBase64Async("image/png"), title, height:screenshot.bitmap.height, width:screenshot.bitmap.width, url} return {image: await screenshot.getBase64Async("image/png"), title, height: screenshot.bitmap.height, width: screenshot.bitmap.width, url}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {title:"Screenshot error", error:{message:"An error occured", instance:error}} throw {title: "Screenshot error", error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -9,42 +9,42 @@ export default async function({login, q, imports, data, account}, {enabled = fal
//Load inputs //Load inputs
let {symbol, interval, duration} = imports.metadata.plugins.stock.inputs({data, account, q}) let {symbol, interval, duration} = imports.metadata.plugins.stock.inputs({data, account, q})
if (!token) if (!token)
throw {error:{message:"A token is required"}} throw {error: {message: "A token is required"}}
if (!symbol) if (!symbol)
throw {error:{message:"A company stock symbol is required"}} throw {error: {message: "A company stock symbol is required"}}
symbol = symbol.toLocaleUpperCase() symbol = symbol.toLocaleUpperCase()
//Query API for company informations //Query API for company informations
console.debug(`metrics/compute/${login}/plugins > stock > querying api for company`) console.debug(`metrics/compute/${login}/plugins > stock > querying api for company`)
const {data:{quoteType:{shortName:company}}} = await imports.axios.get("https://apidojo-yahoo-finance-v1.p.rapidapi.com/stock/v2/get-profile", { const {data: {quoteType: {shortName: company}}} = await imports.axios.get("https://apidojo-yahoo-finance-v1.p.rapidapi.com/stock/v2/get-profile", {
params:{symbol, region:"US"}, params: {symbol, region: "US"},
headers:{"x-rapidapi-key":token}, headers: {"x-rapidapi-key": token},
}) })
//Query API for sotck charts //Query API for sotck charts
console.debug(`metrics/compute/${login}/plugins > stock > querying api for stock`) console.debug(`metrics/compute/${login}/plugins > stock > querying api for stock`)
const {data:{chart:{result:[{meta, timestamp, indicators:{quote:[{close}]}}]}}} = await imports.axios.get("https://apidojo-yahoo-finance-v1.p.rapidapi.com/stock/v2/get-chart", { const {data: {chart: {result: [{meta, timestamp, indicators: {quote: [{close}]}}]}}} = await imports.axios.get("https://apidojo-yahoo-finance-v1.p.rapidapi.com/stock/v2/get-chart", {
params:{interval, symbol, range:duration, region:"US"}, params: {interval, symbol, range: duration, region: "US"},
headers:{"x-rapidapi-key":token}, headers: {"x-rapidapi-key": token},
}) })
const {currency, regularMarketPrice:price, previousClose:previous} = meta const {currency, regularMarketPrice: price, previousClose: previous} = meta
//Generating chart //Generating chart
console.debug(`metrics/compute/${login}/plugins > stock > generating chart`) console.debug(`metrics/compute/${login}/plugins > stock > generating chart`)
const chart = await imports.chartist("line", { const chart = await imports.chartist("line", {
width:480 * (1 + data.large), width: 480 * (1 + data.large),
height:160, height: 160,
showPoint:false, showPoint: false,
axisX:{showGrid:false, labelInterpolationFnc:(value, index) => index % Math.floor(close.length / 4) === 0 ? value : null}, axisX: {showGrid: false, labelInterpolationFnc: (value, index) => index % Math.floor(close.length / 4) === 0 ? value : null},
axisY:{scaleMinSpace:20}, axisY: {scaleMinSpace: 20},
showArea:true, showArea: true,
}, { }, {
labels:timestamp.map(timestamp => new Intl.DateTimeFormat("en-GB", {month:"2-digit", day:"2-digit", hour:"2-digit", minute:"2-digit"}).format(new Date(timestamp * 1000))), labels: timestamp.map(timestamp => new Intl.DateTimeFormat("en-GB", {month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit"}).format(new Date(timestamp * 1000))),
series:[close], series: [close],
}) })
//Results //Results
return {chart, currency, price, previous, delta:price - previous, symbol, company, interval, duration} return {chart, currency, price, previous, delta: price - previous, symbol, company, interval, duration}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
@@ -55,6 +55,6 @@ export default async function({login, q, imports, data, account}, {enabled = fal
message = `API returned ${status}${description ? ` (${description})` : ""}` message = `API returned ${status}${description ? ` (${description})` : ""}`
error = error.response?.data ?? null error = error.response?.data ?? null
} }
throw {error:{message, instance:error}} throw {error: {message, instance: error}}
} }
} }

View File

@@ -8,23 +8,23 @@ export default async function({login, q, imports, data, rest, graphql, queries,
//Load inputs //Load inputs
let {head, base, ignored, contributions, sections, categories} = imports.metadata.plugins.contributors.inputs({data, account, q}) let {head, base, ignored, contributions, sections, categories} = imports.metadata.plugins.contributors.inputs({data, account, q})
const repo = {owner:data.repo.owner.login, repo:data.repo.name} const repo = {owner: data.repo.owner.login, repo: data.repo.name}
ignored.push(...data.shared["users.ignored"]) ignored.push(...data.shared["users.ignored"])
//Retrieve head and base commits //Retrieve head and base commits
console.debug(`metrics/compute/${login}/plugins > contributors > querying api head and base commits`) console.debug(`metrics/compute/${login}/plugins > contributors > querying api head and base commits`)
const ref = { const ref = {
head:(await graphql(queries.contributors.commit({...repo, expression:head}))).repository.object, head: (await graphql(queries.contributors.commit({...repo, expression: head}))).repository.object,
base:(await graphql(queries.contributors.commit({...repo, expression:base}))).repository.object, base: (await graphql(queries.contributors.commit({...repo, expression: base}))).repository.object,
} }
//Get commit activity //Get commit activity
console.debug(`metrics/compute/${login}/plugins > contributors > querying api for commits between [${ref.base?.abbreviatedOid ?? null}] and [${ref.head?.abbreviatedOid ?? null}]`) console.debug(`metrics/compute/${login}/plugins > contributors > querying api for commits between [${ref.base?.abbreviatedOid ?? null}] and [${ref.head?.abbreviatedOid ?? null}]`)
const commits = [] const commits = []
for (let page = 1; ; page++) { for (let page = 1;; page++) {
console.debug(`metrics/compute/${login}/plugins > contributors > loading page ${page}`) console.debug(`metrics/compute/${login}/plugins > contributors > loading page ${page}`)
try { try {
const {data:loaded} = await rest.repos.listCommits({...repo, per_page:100, page}) const {data: loaded} = await rest.repos.listCommits({...repo, per_page: 100, page})
if (loaded.map(({sha}) => sha).includes(ref.base?.oid)) { if (loaded.map(({sha}) => sha).includes(ref.base?.oid)) {
console.debug(`metrics/compute/${login}/plugins > contributors > reached ${ref.base?.oid}`) console.debug(`metrics/compute/${login}/plugins > contributors > reached ${ref.base?.oid}`)
commits.push(...loaded.slice(0, loaded.map(({sha}) => sha).indexOf(ref.base.oid))) commits.push(...loaded.slice(0, loaded.map(({sha}) => sha).indexOf(ref.base.oid)))
@@ -50,13 +50,13 @@ export default async function({login, q, imports, data, rest, graphql, queries,
//Compute contributors and contributions //Compute contributors and contributions
let contributors = {} let contributors = {}
for (const {author:{login, avatar_url:avatar}, commit:{message = "", author:{email = ""} = {}}} of commits) { for (const {author: {login, avatar_url: avatar}, commit: {message = "", author: {email = ""} = {}}} of commits) {
if ((!login) || (ignored.includes(login)) || (ignored.includes(email))) { if ((!login) || (ignored.includes(login)) || (ignored.includes(email))) {
console.debug(`metrics/compute/${login}/plugins > contributors > ignored contributor "${login}"`) console.debug(`metrics/compute/${login}/plugins > contributors > ignored contributor "${login}"`)
continue continue
} }
if (!(login in contributors)) if (!(login in contributors))
contributors[login] = {avatar:await imports.imgb64(avatar), contributions:1, pr:[]} contributors[login] = {avatar: await imports.imgb64(avatar), contributions: 1, pr: []}
else { else {
contributors[login].contributions++ contributors[login].contributions++
contributors[login].pr.push(...(message.match(/(?<=[(])#\d+(?=[)])/g) ?? [])) contributors[login].pr.push(...(message.match(/(?<=[(])#\d+(?=[)])/g) ?? []))
@@ -78,8 +78,8 @@ export default async function({login, q, imports, data, rest, graphql, queries,
try { try {
//Git clone into temporary directory //Git clone into temporary directory
await imports.fs.rm(path, {recursive:true, force:true}) await imports.fs.rm(path, {recursive: true, force: true})
await imports.fs.mkdir(path, {recursive:true}) await imports.fs.mkdir(path, {recursive: true})
const git = await imports.git(path) const git = await imports.git(path)
await git.clone(`https://github.com/${repository}`, ".").status() await git.clone(`https://github.com/${repository}`, ".").status()
@@ -87,7 +87,7 @@ export default async function({login, q, imports, data, rest, graphql, queries,
for (const contributor in contributors) { for (const contributor in contributors) {
//Load edited files by contributor //Load edited files by contributor
const files = [] const files = []
await imports.spawn("git", ["--no-pager", "log", `--author="${contributor}"`, "--regexp-ignore-case", "--no-merges", "--name-only", '--pretty=format:""'], {cwd:path}, { await imports.spawn("git", ["--no-pager", "log", `--author="${contributor}"`, "--regexp-ignore-case", "--no-merges", "--name-only", '--pretty=format:""'], {cwd: path}, {
stdout(line) { stdout(line) {
if (line.trim().length) if (line.trim().length)
files.push(line) files.push(line)
@@ -98,7 +98,7 @@ export default async function({login, q, imports, data, rest, graphql, queries,
for (const file of files) { for (const file of files) {
for (const [category, globs] of Object.entries(categories)) { for (const [category, globs] of Object.entries(categories)) {
for (const glob of [globs].flat(Infinity)) { for (const glob of [globs].flat(Infinity)) {
if (imports.minimatch(file, glob, {nocase:true})) { if (imports.minimatch(file, glob, {nocase: true})) {
types[category].add(contributor) types[category].add(contributor)
continue filesloop continue filesloop
} }
@@ -114,15 +114,15 @@ export default async function({login, q, imports, data, rest, graphql, queries,
finally { finally {
//Cleaning //Cleaning
console.debug(`metrics/compute/${login}/plugins > contributors > cleaning temp dir ${path}`) console.debug(`metrics/compute/${login}/plugins > contributors > cleaning temp dir ${path}`)
await imports.fs.rm(path, {recursive:true, force:true}) await imports.fs.rm(path, {recursive: true, force: true})
} }
} }
//Results //Results
return {head, base, ref, list:contributors, categories:types, contributions, sections} return {head, base, ref, list: contributors, categories: types, contributions, sections}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -6,8 +6,8 @@
//Setup //Setup
export default async function({login, q}, {conf, data, rest, graphql, plugins, queries, account, convert, template}, {pending, imports}) { export default async function({login, q}, {conf, data, rest, graphql, plugins, queries, account, convert, template}, {pending, imports}) {
//Load inputs //Load inputs
const {"config.animations":animations, "config.display":display, "config.timezone":_timezone, "config.base64":_base64, "debug.flags":dflags} = imports.metadata.plugins.core.inputs({data, account, q}) const {"config.animations": animations, "config.display": display, "config.timezone": _timezone, "config.base64": _base64, "debug.flags": dflags} = imports.metadata.plugins.core.inputs({data, account, q})
imports.metadata.templates[template].check({q, account, format:convert}) imports.metadata.templates[template].check({q, account, format: convert})
//Base64 images //Base64 images
if (!_base64) { if (!_base64) {
@@ -17,23 +17,23 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
//Init //Init
const computed = { const computed = {
commits:0, commits: 0,
sponsorships:0, sponsorships: 0,
licenses:{favorite:"", used:{}, about:{}}, licenses: {favorite: "", used: {}, about: {}},
token:{}, token: {},
repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_closed:0, pr_merged:0, forks:0, forked:0, releases:0, deployments:0, environments:0}, repositories: {watchers: 0, stargazers: 0, issues_open: 0, issues_closed: 0, pr_open: 0, pr_closed: 0, pr_merged: 0, forks: 0, forked: 0, releases: 0, deployments: 0, environments: 0},
} }
const avatar = imports.imgb64(data.user.avatarUrl) const avatar = imports.imgb64(data.user.avatarUrl)
data.computed = computed data.computed = computed
console.debug(`metrics/compute/${login} > formatting common metrics`) console.debug(`metrics/compute/${login} > formatting common metrics`)
//Timezone config //Timezone config
const offset = Number(new Date().toLocaleString("fr", {timeZoneName:"short"}).match(/UTC[+](?<offset>\d+)/)?.groups?.offset * 60 * 60 * 1000) || 0 const offset = Number(new Date().toLocaleString("fr", {timeZoneName: "short"}).match(/UTC[+](?<offset>\d+)/)?.groups?.offset * 60 * 60 * 1000) || 0
if (_timezone) { if (_timezone) {
const timezone = {name:_timezone, offset:0} const timezone = {name: _timezone, offset: 0}
data.config.timezone = timezone data.config.timezone = timezone
try { try {
timezone.offset = offset - (Number(new Date().toLocaleString("fr", {timeZoneName:"short", timeZone:timezone.name}).match(/UTC[+](?<offset>\d+)/)?.groups?.offset * 60 * 60 * 1000) || 0) timezone.offset = 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)`) console.debug(`metrics/compute/${login} > timezone set to ${timezone.name} (${timezone.offset > 0 ? "+" : ""}${Math.round(timezone.offset / (60 * 60 * 1000))} hours)`)
} }
catch { catch {
@@ -41,9 +41,9 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
console.debug(`metrics/compute/${login} > failed to use timezone "${timezone.name}"`) console.debug(`metrics/compute/${login} > failed to use timezone "${timezone.name}"`)
} }
} }
else if (process?.env?.TZ) else if (process?.env?.TZ) {
data.config.timezone = {name:process.env.TZ, offset} data.config.timezone = {name: process.env.TZ, offset}
}
//Display //Display
data.large = display === "large" data.large = display === "large"
@@ -60,7 +60,7 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
pending.push((async () => { pending.push((async () => {
try { try {
console.debug(`metrics/compute/${login}/plugins > ${name} > started`) console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries, account}, {extras:conf.settings?.extras?.features ?? conf.settings?.extras?.default ?? false, sandbox:conf.settings?.sandbox ?? false, ...plugins[name]}) data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries, account}, {extras: conf.settings?.extras?.features ?? conf.settings?.extras?.default ?? false, sandbox: conf.settings?.sandbox ?? false, ...plugins[name]})
console.debug(`metrics/compute/${login}/plugins > ${name} > completed`) console.debug(`metrics/compute/${login}/plugins > ${name} > completed`)
} }
catch (error) { catch (error) {
@@ -68,8 +68,8 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
data.plugins[name] = error data.plugins[name] = error
} }
finally { finally {
const result = {name, result:data.plugins[name]} const result = {name, result: data.plugins[name]}
console.debug(imports.util.inspect(result, {depth:Infinity, maxStringLength:256, getters:true})) console.debug(imports.util.inspect(result, {depth: Infinity, maxStringLength: 256, getters: true}))
return result return result
} }
})()) })())
@@ -108,7 +108,7 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
const months = diff.getUTCMonth() - new Date(0).getUTCMonth() const months = diff.getUTCMonth() - new Date(0).getUTCMonth()
const days = diff.getUTCDate() - new Date(0).getUTCDate() const days = diff.getUTCDate() - new Date(0).getUTCDate()
computed.registered = {years:years + days / 365.25, months} computed.registered = {years: years + days / 365.25, months}
computed.registration = years ? `${years} year${imports.s(years)} ago` : months ? `${months} month${imports.s(months)} ago` : `${days} day${imports.s(days)} ago` computed.registration = years ? `${years} year${imports.s(years)} ago` : months ? `${months} month${imports.s(months)} ago` : `${days} day${imports.s(days)} ago`
computed.cakeday = (years >= 1 && months === 0 && days === 0) ? true : false computed.cakeday = (years >= 1 && months === 0 && days === 0) ? true : false
@@ -129,9 +129,9 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
//Meta //Meta
data.meta = { data.meta = {
version:conf.package.version, version: conf.package.version,
author:conf.package.author, author: conf.package.author,
generated:imports.format.date(new Date(), {date:true, time:true}), generated: imports.format.date(new Date(), {date: true, time: true}),
} }
//Debug flags //Debug flags
@@ -146,7 +146,8 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
if (dflags.includes("--halloween")) { if (dflags.includes("--halloween")) {
console.debug(`metrics/compute/${login} > applying dflag --halloween`) console.debug(`metrics/compute/${login} > applying dflag --halloween`)
//Haloween color replacer //Haloween color replacer
const halloween = content => content const halloween = content =>
content
.replace(/--color-calendar-graph/g, "--color-calendar-halloween-graph") .replace(/--color-calendar-graph/g, "--color-calendar-halloween-graph")
.replace(/#9be9a8/gi, "var(--color-calendar-halloween-graph-day-L1-bg)") .replace(/#9be9a8/gi, "var(--color-calendar-halloween-graph-day-L1-bg)")
.replace(/#40c463/gi, "var(--color-calendar-halloween-graph-day-L2-bg)") .replace(/#40c463/gi, "var(--color-calendar-halloween-graph-day-L2-bg)")
@@ -160,7 +161,7 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
await Promise.all(waiting) await Promise.all(waiting)
if (data.plugins.isocalendar?.svg) if (data.plugins.isocalendar?.svg)
data.plugins.isocalendar.svg = halloween(data.plugins.isocalendar.svg) data.plugins.isocalendar.svg = halloween(data.plugins.isocalendar.svg)
return {name:"dflag.halloween", result:true} return {name: "dflag.halloween", result: true}
})()) })())
} }
if (dflags.includes("--error")) { if (dflags.includes("--error")) {

View File

@@ -7,9 +7,9 @@ export default async function({login, q, imports, graphql, queries, data, accoun
return null return null
//Load inputs //Load inputs
const {categories:_categories, "categories.limit":_categories_limit} = imports.metadata.plugins.discussions.inputs({data, account, q}) const {categories: _categories, "categories.limit": _categories_limit} = imports.metadata.plugins.discussions.inputs({data, account, q})
const discussions = {categories:{}, upvotes:{discussions:0, comments:0}} const discussions = {categories: {}, upvotes: {discussions: 0, comments: 0}}
discussions.display = {categories:_categories ? {limit:_categories_limit || Infinity} : null} discussions.display = {categories: _categories ? {limit: _categories_limit || Infinity} : null}
//Fetch general statistics //Fetch general statistics
const stats = Object.fromEntries(Object.entries((await graphql(queries.discussions.statistics({login}))).user).map(([key, value]) => [key, value.totalCount])) const stats = Object.fromEntries(Object.entries((await graphql(queries.discussions.statistics({login}))).user).map(([key, value]) => [key, value.totalCount]))
@@ -23,7 +23,7 @@ export default async function({login, q, imports, graphql, queries, data, accoun
let pushed = 0 let pushed = 0
do { do {
console.debug(`metrics/compute/${login}/discussions > retrieving discussions after ${cursor}`) console.debug(`metrics/compute/${login}/discussions > retrieving discussions after ${cursor}`)
const {user:{repositoryDiscussions:{edges = [], nodes = []} = {}}} = await graphql(queries.discussions.categories({login, after:cursor ? `after: "${cursor}"` : ""})) const {user: {repositoryDiscussions: {edges = [], nodes = []} = {}}} = await graphql(queries.discussions.categories({login, after: cursor ? `after: "${cursor}"` : ""}))
cursor = edges?.[edges?.length - 1]?.cursor cursor = edges?.[edges?.length - 1]?.cursor
fetched.push(...nodes) fetched.push(...nodes)
pushed = nodes.length pushed = nodes.length
@@ -34,7 +34,7 @@ export default async function({login, q, imports, graphql, queries, data, accoun
fetched.map(({upvoteCount}) => discussions.upvotes.discussions += upvoteCount) fetched.map(({upvoteCount}) => discussions.upvotes.discussions += upvoteCount)
//Compute favorite category //Compute favorite category
for (const category of [...fetched.map(({category:{emoji, name}}) => `${imports.emoji.get(emoji) ?? emoji} ${name}`)]) for (const category of [...fetched.map(({category: {emoji, name}}) => `${imports.emoji.get(emoji) ?? emoji} ${name}`)])
categories[category] = (categories[category] ?? 0) + 1 categories[category] = (categories[category] ?? 0) + 1
const categoryEntries = Object.entries(categories).sort((a, b) => b[1] - a[1]) const categoryEntries = Object.entries(categories).sort((a, b) => b[1] - a[1])
discussions.categories.stats = Object.fromEntries(categoryEntries) discussions.categories.stats = Object.fromEntries(categoryEntries)
@@ -48,7 +48,7 @@ export default async function({login, q, imports, graphql, queries, data, accoun
let pushed = 0 let pushed = 0
do { do {
console.debug(`metrics/compute/${login}/discussions > retrieving comments after ${cursor}`) console.debug(`metrics/compute/${login}/discussions > retrieving comments after ${cursor}`)
const {user:{repositoryDiscussionComments:{edges = [], nodes = []} = {}}} = await graphql(queries.discussions.comments({login, after:cursor ? `after: "${cursor}"` : ""})) const {user: {repositoryDiscussionComments: {edges = [], nodes = []} = {}}} = await graphql(queries.discussions.comments({login, after: cursor ? `after: "${cursor}"` : ""}))
cursor = edges?.[edges?.length - 1]?.cursor cursor = edges?.[edges?.length - 1]?.cursor
fetched.push(...nodes) fetched.push(...nodes)
pushed = nodes.length pushed = nodes.length
@@ -64,6 +64,6 @@ export default async function({login, q, imports, graphql, queries, data, accoun
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -12,7 +12,7 @@ export default async function({login, data, computed, imports, q, graphql, queri
//Define getters //Define getters
const followup = { const followup = {
sections, sections,
issues:{ issues: {
get count() { get count() {
return this.open + this.closed + this.drafts + this.skipped return this.open + this.closed + this.drafts + this.skipped
}, },
@@ -22,16 +22,16 @@ export default async function({login, data, computed, imports, q, graphql, queri
get closed() { get closed() {
return computed.repositories.issues_closed return computed.repositories.issues_closed
}, },
drafts:0, drafts: 0,
skipped:0, skipped: 0,
collaborators:{ collaborators: {
open:0, open: 0,
closed:0, closed: 0,
drafts:0, drafts: 0,
skipped:0, skipped: 0,
}, },
}, },
pr:{ pr: {
get count() { get count() {
return this.open + this.closed + this.merged + this.drafts return this.open + this.closed + this.merged + this.drafts
}, },
@@ -44,12 +44,12 @@ export default async function({login, data, computed, imports, q, graphql, queri
get merged() { get merged() {
return computed.repositories.pr_merged return computed.repositories.pr_merged
}, },
drafts:0, drafts: 0,
collaborators:{ collaborators: {
open:0, open: 0,
closed:0, closed: 0,
merged:0, merged: 0,
drafts:0, drafts: 0,
}, },
}, },
} }
@@ -59,15 +59,15 @@ export default async function({login, data, computed, imports, q, graphql, queri
//Indepth mode //Indepth mode
if (indepth) { if (indepth) {
console.debug(`metrics/compute/${login}/plugins > followup > indepth`) console.debug(`metrics/compute/${login}/plugins > followup > indepth`)
followup.indepth = {repositories:{}} followup.indepth = {repositories: {}}
//Process repositories //Process repositories
for (const {name:repo, owner:{login:owner}} of data.user.repositories.nodes) { for (const {name: repo, owner: {login: owner}} of data.user.repositories.nodes) {
try { try {
console.debug(`metrics/compute/${login}/plugins > followup > processing ${owner}/${repo}`) console.debug(`metrics/compute/${login}/plugins > followup > processing ${owner}/${repo}`)
followup.indepth.repositories[`${owner}/${repo}`] = {stats:{}} followup.indepth.repositories[`${owner}/${repo}`] = {stats: {}}
//Fetch users with push access //Fetch users with push access
let {repository:{collaborators:{nodes:collaborators}}} = await graphql(queries.followup["repository.collaborators"]({repo, owner})).catch(() => ({repository:{collaborators:{nodes:[{login:owner}]}}})) let {repository: {collaborators: {nodes: collaborators}}} = await graphql(queries.followup["repository.collaborators"]({repo, owner})).catch(() => ({repository: {collaborators: {nodes: [{login: owner}]}}}))
console.debug(`metrics/compute/${login}/plugins > followup > found ${collaborators.length} collaborators`) console.debug(`metrics/compute/${login}/plugins > followup > found ${collaborators.length} collaborators`)
followup.indepth.repositories[`${owner}/${repo}`].collaborators = collaborators.map(({login}) => login) followup.indepth.repositories[`${owner}/${repo}`].collaborators = collaborators.map(({login}) => login)
//Fetch issues and pull requests created by collaborators //Fetch issues and pull requests created by collaborators
@@ -75,7 +75,7 @@ export default async function({login, data, computed, imports, q, graphql, queri
const stats = await graphql(queries.followup.repository({repo, owner, collaborators})) const stats = await graphql(queries.followup.repository({repo, owner, collaborators}))
followup.indepth.repositories[`${owner}/${repo}`] = stats followup.indepth.repositories[`${owner}/${repo}`] = stats
//Aggregate global stats //Aggregate global stats
for (const [key, {issueCount:count}] of Object.entries(stats)) { for (const [key, {issueCount: count}] of Object.entries(stats)) {
const [section, type] = key.split("_") const [section, type] = key.split("_")
followup[section].collaborators[type] += count followup[section].collaborators[type] += count
} }
@@ -92,23 +92,23 @@ export default async function({login, data, computed, imports, q, graphql, queri
if ((account === "user") && (sections.includes("user"))) { if ((account === "user") && (sections.includes("user"))) {
const search = await graphql(queries.followup.user({login})) const search = await graphql(queries.followup.user({login}))
followup.user = { followup.user = {
issues:{ issues: {
get count() { get count() {
return this.open + this.closed + this.drafts + this.skipped return this.open + this.closed + this.drafts + this.skipped
}, },
open:search.issues_open.issueCount, open: search.issues_open.issueCount,
closed:search.issues_closed.issueCount, closed: search.issues_closed.issueCount,
drafts:search.issues_drafts.issueCount, drafts: search.issues_drafts.issueCount,
skipped:search.issues_skipped.issueCount, skipped: search.issues_skipped.issueCount,
}, },
pr:{ pr: {
get count() { get count() {
return this.open + this.closed + this.merged + this.drafts return this.open + this.closed + this.merged + this.drafts
}, },
open:search.pr_open.issueCount, open: search.pr_open.issueCount,
closed:search.pr_closed.issueCount, closed: search.pr_closed.issueCount,
merged:search.pr_merged.issueCount, merged: search.pr_merged.issueCount,
drafts:search.pr_drafts.issueCount, drafts: search.pr_drafts.issueCount,
}, },
} }
} }
@@ -118,6 +118,6 @@ export default async function({login, data, computed, imports, q, graphql, queri
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -17,7 +17,7 @@ export default async function({login, data, graphql, q, imports, queries, accoun
let pushed = 0 let pushed = 0
do { do {
console.debug(`metrics/compute/${login}/plugins > gists > retrieving gists after ${cursor}`) console.debug(`metrics/compute/${login}/plugins > gists > retrieving gists after ${cursor}`)
const {user:{gists:{edges, nodes, totalCount}}} = await graphql(queries.gists({login, after:cursor ? `after: "${cursor}"` : ""})) const {user: {gists: {edges, nodes, totalCount}}} = await graphql(queries.gists({login, after: cursor ? `after: "${cursor}"` : ""}))
cursor = edges?.[edges?.length - 1]?.cursor cursor = edges?.[edges?.length - 1]?.cursor
gists.push(...nodes) gists.push(...nodes)
gists.totalCount = totalCount gists.totalCount = totalCount
@@ -41,12 +41,12 @@ export default async function({login, data, graphql, q, imports, queries, accoun
} }
//Results //Results
return {totalCount:gists.totalCount, stargazers, forks, files, comments} return {totalCount: gists.totalCount, stargazers, forks, files, comments}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -1,5 +1,5 @@
//Legacy import //Legacy import
import {recent as recent_analyzer} from "./../languages/analyzers.mjs" import { recent as recent_analyzer } from "./../languages/analyzers.mjs"
//Setup //Setup
export default async function({login, data, rest, imports, q, account}, {enabled = false, extras = false, ...defaults} = {}) { export default async function({login, data, rest, imports, q, account}, {enabled = false, extras = false, ...defaults} = {}) {
@@ -10,10 +10,10 @@ export default async function({login, data, rest, imports, q, account}, {enabled
return null return null
//Load inputs //Load inputs
let {from, days, facts, charts, "charts.type":_charts, trim} = imports.metadata.plugins.habits.inputs({data, account, q}, defaults) let {from, days, facts, charts, "charts.type": _charts, trim} = imports.metadata.plugins.habits.inputs({data, account, q}, defaults)
//Initialization //Initialization
const habits = {facts, charts, trim, lines:{average:{chars:0}}, commits:{fetched:0, hour:NaN, hours:{}, day:NaN, days:{}}, indents:{style:"", spaces:0, tabs:0}, linguist:{available:false, ordered:[], languages:{}}} const habits = {facts, charts, trim, lines: {average: {chars: 0}}, commits: {fetched: 0, hour: NaN, hours: {}, day: NaN, days: {}}, indents: {style: "", spaces: 0, tabs: 0}, linguist: {available: false, ordered: [], languages: {}}}
const pages = Math.ceil(from / 100) const pages = Math.ceil(from / 100)
const offset = data.config.timezone?.offset ?? 0 const offset = data.config.timezone?.offset ?? 0
@@ -23,7 +23,7 @@ export default async function({login, data, rest, imports, q, account}, {enabled
try { try {
for (let page = 1; page <= pages; page++) { for (let page = 1; page <= pages; page++) {
console.debug(`metrics/compute/${login}/plugins > habits > loading 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) events.push(...(await rest.activity.listEventsForAuthenticatedUser({username: login, per_page: 100, page})).data)
} }
} }
catch { catch {
@@ -51,8 +51,8 @@ export default async function({login, data, rest, imports, q, account}, {enabled
] ]
.filter(({status}) => status === "fulfilled") .filter(({status}) => status === "fulfilled")
.map(({value}) => value) .map(({value}) => value)
.flatMap(files => files.map(file => ({name:imports.paths.basename(file.filename), patch:file.patch ?? ""}))) .flatMap(files => files.map(file => ({name: imports.paths.basename(file.filename), patch: file.patch ?? ""})))
.map(({name, patch}) => ({name, patch:patch.split("\n").filter(line => /^[+]/.test(line)).map(line => line.substring(1)).join("\n")})) .map(({name, patch}) => ({name, patch: patch.split("\n").filter(line => /^[+]/.test(line)).map(line => line.substring(1)).join("\n")}))
//Commit day //Commit day
{ {
@@ -103,41 +103,41 @@ export default async function({login, data, rest, imports, q, account}, {enabled
if (patches.length) { if (patches.length) {
//Call language analyzer (note: using content from other plugin is usually disallowed, this is mostly for legacy purposes) //Call language analyzer (note: using content from other plugin is usually disallowed, this is mostly for legacy purposes)
habits.linguist.available = true habits.linguist.available = true
const {total, stats} = await recent_analyzer({login, data, imports, rest, account}, {days, load:from || 1000, tempdir:"habits"}) const {total, stats} = await recent_analyzer({login, data, imports, rest, account}, {days, load: from || 1000, tempdir: "habits"})
habits.linguist.languages = Object.fromEntries(Object.entries(stats).map(([language, value]) => [language, value / total])) habits.linguist.languages = Object.fromEntries(Object.entries(stats).map(([language, value]) => [language, value / total]))
habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([_an, a], [_bn, b]) => b - a) habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([_an, a], [_bn, b]) => b - a)
} }
else else {
console.debug(`metrics/compute/${login}/plugins > habits > linguist not available`) console.debug(`metrics/compute/${login}/plugins > habits > linguist not available`)
}
} }
//Generating charts with chartist //Generating charts with chartist
if (_charts === "chartist") { if (_charts === "chartist") {
console.debug(`metrics/compute/${login}/plugins > habits > generating charts`) console.debug(`metrics/compute/${login}/plugins > habits > generating charts`)
habits.charts = await Promise.all([ habits.charts = await Promise.all([
{type:"line", data:{...empty(24), ...Object.fromEntries(Object.entries(habits.commits.hours).filter(([k]) => !Number.isNaN(+k)))}, low:0, high:habits.commits.hours.max}, {type: "line", data: {...empty(24), ...Object.fromEntries(Object.entries(habits.commits.hours).filter(([k]) => !Number.isNaN(+k)))}, low: 0, high: habits.commits.hours.max},
{type:"line", data:{...empty(7), ...Object.fromEntries(Object.entries(habits.commits.days).filter(([k]) => !Number.isNaN(+k)))}, low:0, high:habits.commits.days.max, labels:["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], half:true}, {type: "line", data: {...empty(7), ...Object.fromEntries(Object.entries(habits.commits.days).filter(([k]) => !Number.isNaN(+k)))}, low: 0, high: habits.commits.days.max, labels: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], half: true},
{type:"pie", data:habits.linguist.languages, half:true}, {type: "pie", data: habits.linguist.languages, half: true},
].map(({type, data, high, low, ref, labels = {}, half = false}) => { ].map(({type, data, high, low, ref, labels = {}, half = false}) => {
const options = { const options = {
width:480 * (half ? 0.45 : 1), width: 480 * (half ? 0.45 : 1),
height:160, height: 160,
fullWidth:true, fullWidth: true,
} }
const values = { const values = {
labels:Object.keys(data).map(key => labels[key] ?? key), labels: Object.keys(data).map(key => labels[key] ?? key),
series:Object.values(data), series: Object.values(data),
} }
if (type === "line") { if (type === "line") {
Object.assign(options, { Object.assign(options, {
showPoint:true, showPoint: true,
axisX:{showGrid:false}, axisX: {showGrid: false},
axisY:{showLabel:false, offset:20, labelInterpolationFnc:value => imports.format(value), high, low, referenceValue:ref}, axisY: {showLabel: false, offset: 20, labelInterpolationFnc: value => imports.format(value), high, low, referenceValue: ref},
showArea:true, showArea: true,
}) })
Object.assign(values, { Object.assign(values, {
series:[Object.values(data)], series: [Object.values(data)],
}) })
} }
return imports.chartist(type, options, values) return imports.chartist(type, options, values)
@@ -166,7 +166,7 @@ export default async function({login, data, rest, imports, q, account}, {enabled
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -10,22 +10,22 @@ export default async function({login, q, imports, data, graphql, queries, accoun
let {title} = imports.metadata.plugins.introduction.inputs({data, account, q}) let {title} = imports.metadata.plugins.introduction.inputs({data, account, q})
//Context //Context
let context = {mode:account, login} let context = {mode: account, login}
if (q.repo) { if (q.repo) {
console.debug(`metrics/compute/${login}/plugins > people > switched to repository mode`) console.debug(`metrics/compute/${login}/plugins > people > switched to repository mode`)
const {owner, repo} = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})).shift() const {owner, repo} = data.user.repositories.nodes.map(({name: repo, owner: {login: owner}}) => ({repo, owner})).shift()
context = {...context, mode:"repository", owner, repo} context = {...context, mode: "repository", owner, repo}
} }
//Querying API //Querying API
console.debug(`metrics/compute/${login}/plugins > introduction > querying api`) console.debug(`metrics/compute/${login}/plugins > introduction > querying api`)
const text = (await graphql(queries.introduction[context.mode](context)))[context.mode][{user:"bio", organization:"description", repository:"description"}[context.mode]] const text = (await graphql(queries.introduction[context.mode](context)))[context.mode][{user: "bio", organization: "description", repository: "description"}[context.mode]]
//Results //Results
return {mode:context.mode, title, text} return {mode: context.mode, title, text}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -27,8 +27,8 @@ export default async function({login, data, graphql, q, imports, queries, accoun
//Compute contribution calendar, highest contributions in a day, streaks and average commits per day //Compute contribution calendar, highest contributions in a day, streaks and average commits per day
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing stats`) console.debug(`metrics/compute/${login}/plugins > isocalendar > computing stats`)
const calendar = {weeks:[]} const calendar = {weeks: []}
const {streak, max, average} = await statistics({login, graphql, queries, start, end:now, calendar}) const {streak, max, average} = await statistics({login, graphql, queries, start, end: now, calendar})
const reference = Math.max(...calendar.weeks.flatMap(({contributionDays}) => contributionDays.map(({contributionCount}) => contributionCount))) const reference = Math.max(...calendar.weeks.flatMap(({contributionDays}) => contributionDays.map(({contributionCount}) => contributionCount)))
//Compute SVG //Compute SVG
@@ -43,8 +43,7 @@ export default async function({login, data, graphql, q, imports, queries, accoun
<feComponentTransfer> <feComponentTransfer>
${[..."RGB"].map(channel => `<feFunc${channel} type="linear" slope="${1 - k * 0.4}" />`).join("")} ${[..."RGB"].map(channel => `<feFunc${channel} type="linear" slope="${1 - k * 0.4}" />`).join("")}
</feComponentTransfer> </feComponentTransfer>
</filter>` </filter>`)
)
.join("") .join("")
} }
<g transform="scale(4) translate(12, 0)">` <g transform="scale(4) translate(12, 0)">`
@@ -75,13 +74,13 @@ export default async function({login, data, graphql, q, imports, queries, accoun
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }
/**Compute max and current streaks */ /**Compute max and current streaks */
async function statistics({login, graphql, queries, start, end, calendar}) { async function statistics({login, graphql, queries, start, end, calendar}) {
let average = 0, max = 0, streak = {max:0, current:0}, values = [] let average = 0, max = 0, streak = {max: 0, current: 0}, values = []
//Load contribution calendar //Load contribution calendar
for (let from = new Date(start); from < end;) { for (let from = new Date(start); from < end;) {
//Set date range //Set date range
@@ -97,7 +96,7 @@ async function statistics({login, graphql, queries, start, end, calendar}) {
dto.setUTCMilliseconds(999) dto.setUTCMilliseconds(999)
//Fetch data from api //Fetch data from api
console.debug(`metrics/compute/${login}/plugins > isocalendar > loading calendar from "${from.toISOString()}" to "${dto.toISOString()}"`) console.debug(`metrics/compute/${login}/plugins > isocalendar > loading calendar from "${from.toISOString()}" to "${dto.toISOString()}"`)
const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(queries.isocalendar.calendar({login, from:from.toISOString(), to:dto.toISOString()})) const {user: {calendar: {contributionCalendar: {weeks}}}} = await graphql(queries.isocalendar.calendar({login, from: from.toISOString(), to: dto.toISOString()}))
calendar.weeks.push(...weeks) calendar.weeks.push(...weeks)
//Set next date range start //Set next date range start
from = new Date(to) from = new Date(to)

View File

@@ -5,7 +5,7 @@ import linguist from "linguist-js"
export async function indepth({login, data, imports, repositories, gpg}, {skipped, categories, timeout}) { export async function indepth({login, data, imports, repositories, gpg}, {skipped, categories, timeout}) {
return new Promise(async solve => { return new Promise(async solve => {
//Results //Results
const results = {partial:false, total:0, lines:{}, stats:{}, colors:{}, commits:0, files:0, missed:{lines:0, bytes:0, commits:0}, verified:{signature:0}} const results = {partial: false, total: 0, lines: {}, stats: {}, colors: {}, commits: 0, files: 0, missed: {lines: 0, bytes: 0, commits: 0}, verified: {signature: 0}}
//Timeout //Timeout
if (Number.isFinite(timeout)) { if (Number.isFinite(timeout)) {
@@ -27,9 +27,9 @@ export async function indepth({login, data, imports, repositories, gpg}, {skippe
console.debug(`metrics/compute/${login}/plugins > languages > importing gpg ${id}`) console.debug(`metrics/compute/${login}/plugins > languages > importing gpg ${id}`)
await imports.run(`gpg --import ${path}`) await imports.run(`gpg --import ${path}`)
} }
else else {
console.debug(`metrics/compute/${login}/plugins > languages > skipping import of gpg ${id}`) console.debug(`metrics/compute/${login}/plugins > languages > skipping import of gpg ${id}`)
}
} }
catch (error) { catch (error) {
console.debug(`metrics/compute/${login}/plugins > languages > indepth > an error occured while importing gpg ${id}, skipping...`) console.debug(`metrics/compute/${login}/plugins > languages > indepth > an error occured while importing gpg ${id}, skipping...`)
@@ -37,7 +37,7 @@ export async function indepth({login, data, imports, repositories, gpg}, {skippe
finally { finally {
//Cleaning //Cleaning
console.debug(`metrics/compute/${login}/plugins > languages > indepth > cleaning ${path}`) console.debug(`metrics/compute/${login}/plugins > languages > indepth > cleaning ${path}`)
await imports.fs.rm(path, {recursive:true, force:true}).catch(error => console.debug(`metrics/compute/${login}/plugins > languages > indepth > failed to clean ${path} (${error})`)) await imports.fs.rm(path, {recursive: true, force: true}).catch(error => console.debug(`metrics/compute/${login}/plugins > languages > indepth > failed to clean ${path} (${error})`))
} }
} }
@@ -64,8 +64,8 @@ export async function indepth({login, data, imports, repositories, gpg}, {skippe
//Process //Process
try { try {
//Git clone into temporary directory //Git clone into temporary directory
await imports.fs.rm(path, {recursive:true, force:true}) await imports.fs.rm(path, {recursive: true, force: true})
await imports.fs.mkdir(path, {recursive:true}) await imports.fs.mkdir(path, {recursive: true})
const git = await imports.git(path) const git = await imports.git(path)
await git.clone(`https://github.com/${repo}`, ".").status() await git.clone(`https://github.com/${repo}`, ".").status()
@@ -78,7 +78,7 @@ export async function indepth({login, data, imports, repositories, gpg}, {skippe
finally { finally {
//Cleaning //Cleaning
console.debug(`metrics/compute/${login}/plugins > languages > indepth > cleaning temp dir ${path}`) console.debug(`metrics/compute/${login}/plugins > languages > indepth > cleaning temp dir ${path}`)
await imports.fs.rm(path, {recursive:true, force:true}).catch(error => console.debug(`metrics/compute/${login}/plugins > languages > indepth > failed to clean ${path} (${error})`)) await imports.fs.rm(path, {recursive: true, force: true}).catch(error => console.debug(`metrics/compute/${login}/plugins > languages > indepth > failed to clean ${path} (${error})`))
} }
} }
solve(results) solve(results)
@@ -89,7 +89,7 @@ export async function indepth({login, data, imports, repositories, gpg}, {skippe
export async function recent({login, data, imports, rest, account}, {skipped = [], categories, days = 0, load = 0, tempdir = "recent", timeout}) { export async function recent({login, data, imports, rest, account}, {skipped = [], categories, days = 0, load = 0, tempdir = "recent", timeout}) {
return new Promise(async solve => { return new Promise(async solve => {
//Results //Results
const results = {partial:false, total:0, lines:{}, stats:{}, colors:{}, commits:0, files:0, missed:{lines:0, bytes:0, commits:0}, days} const results = {partial: false, total: 0, lines: {}, stats: {}, colors: {}, commits: 0, files: 0, missed: {lines: 0, bytes: 0, commits: 0}, days}
//Timeout //Timeout
if (Number.isFinite(timeout)) { if (Number.isFinite(timeout)) {
@@ -109,10 +109,10 @@ export async function recent({login, data, imports, rest, account}, {skipped = [
for (let page = 1; page <= pages; page++) { for (let page = 1; page <= pages; page++) {
console.debug(`metrics/compute/${login}/plugins > languages > loading page ${page}`) console.debug(`metrics/compute/${login}/plugins > languages > loading page ${page}`)
commits.push( commits.push(
...(await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data ...(await rest.activity.listEventsForAuthenticatedUser({username: login, per_page: 100, page})).data
.filter(({type}) => type === "PushEvent") .filter(({type}) => type === "PushEvent")
.filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase()) .filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase())
.filter(({repo:{name:repo}}) => (!skipped.includes(repo.toLocaleLowerCase())) && (!skipped.includes(repo.toLocaleLowerCase().split("/").pop()))) .filter(({repo: {name: repo}}) => (!skipped.includes(repo.toLocaleLowerCase())) && (!skipped.includes(repo.toLocaleLowerCase().split("/").pop())))
.filter(({created_at}) => new Date(created_at) > new Date(Date.now() - days * 24 * 60 * 60 * 1000)), .filter(({created_at}) => new Date(created_at) > new Date(Date.now() - days * 24 * 60 * 60 * 1000)),
) )
} }
@@ -139,8 +139,8 @@ export async function recent({login, data, imports, rest, account}, {skipped = [
.map(({value}) => value) .map(({value}) => value)
.filter(({parents}) => parents.length <= 1) .filter(({parents}) => parents.length <= 1)
.map(({files}) => files) .map(({files}) => files)
.flatMap(files => files.map(file => ({name:imports.paths.basename(file.filename), directory:imports.paths.dirname(file.filename), patch:file.patch ?? "", repo:file.raw_url?.match(/(?<=^https:..github.com\/)(?<repo>.*)(?=\/raw)/)?.groups.repo ?? "_"}))) .flatMap(files => files.map(file => ({name: imports.paths.basename(file.filename), directory: imports.paths.dirname(file.filename), patch: file.patch ?? "", repo: file.raw_url?.match(/(?<=^https:..github.com\/)(?<repo>.*)(?=\/raw)/)?.groups.repo ?? "_"})))
.map(({name, directory, patch, repo}) => ({name, directory:`${repo.replace(/[/]/g, "@")}/${directory}`, patch:patch.split("\n").filter(line => /^[+]/.test(line)).map(line => line.substring(1)).join("\n")})) .map(({name, directory, patch, repo}) => ({name, directory: `${repo.replace(/[/]/g, "@")}/${directory}`, patch: patch.split("\n").filter(line => /^[+]/.test(line)).map(line => line.substring(1)).join("\n")}))
//Temporary directory //Temporary directory
const path = imports.paths.join(imports.os.tmpdir(), `${data.user.databaseId}-${tempdir}`) const path = imports.paths.join(imports.os.tmpdir(), `${data.user.databaseId}-${tempdir}`)
@@ -149,10 +149,10 @@ export async function recent({login, data, imports, rest, account}, {skipped = [
//Process //Process
try { try {
//Save patches in temporary directory matching respective repository and filename //Save patches in temporary directory matching respective repository and filename
await imports.fs.rm(path, {recursive:true, force:true}) await imports.fs.rm(path, {recursive: true, force: true})
await imports.fs.mkdir(path, {recursive:true}) await imports.fs.mkdir(path, {recursive: true})
await Promise.all(patches.map(async ({name, directory, patch}) => { await Promise.all(patches.map(async ({name, directory, patch}) => {
await imports.fs.mkdir(imports.paths.join(path, directory), {recursive:true}) await imports.fs.mkdir(imports.paths.join(path, directory), {recursive: true})
await imports.fs.writeFile(imports.paths.join(path, directory, name), patch) await imports.fs.writeFile(imports.paths.join(path, directory, name), patch)
})) }))
@@ -177,7 +177,7 @@ export async function recent({login, data, imports, rest, account}, {skipped = [
await git.init().add(".").addConfig("user.name", data.shared["commits.authoring"]?.[0] ?? login).addConfig("user.email", "<>").commit("linguist").status() await git.init().add(".").addConfig("user.name", data.shared["commits.authoring"]?.[0] ?? login).addConfig("user.email", "<>").commit("linguist").status()
//Analyze repository //Analyze repository
await analyze(arguments[0], {results, path:imports.paths.join(path, directory), categories}) await analyze(arguments[0], {results, path: imports.paths.join(path, directory), categories})
//Since we reproduce a "partial repository" with a single commit, use number of commits retrieved instead //Since we reproduce a "partial repository" with a single commit, use number of commits retrieved instead
results.commits = commits.length results.commits = commits.length
@@ -189,7 +189,7 @@ export async function recent({login, data, imports, rest, account}, {skipped = [
finally { finally {
//Cleaning //Cleaning
console.debug(`metrics/compute/${login}/plugins > languages > cleaning temp dir ${path}`) console.debug(`metrics/compute/${login}/plugins > languages > cleaning temp dir ${path}`)
await imports.fs.rm(path, {recursive:true, force:true}).catch(error => console.debug(`metrics/compute/${login}/plugins > languages > indepth > failed to clean ${path} (${error})`)) await imports.fs.rm(path, {recursive: true, force: true}).catch(error => console.debug(`metrics/compute/${login}/plugins > languages > indepth > failed to clean ${path} (${error})`))
} }
solve(results) solve(results)
}) })
@@ -199,7 +199,7 @@ export async function recent({login, data, imports, rest, account}, {skipped = [
async function analyze({login, imports, data}, {results, path, categories = ["programming", "markup"]}) { async function analyze({login, imports, data}, {results, path, categories = ["programming", "markup"]}) {
//Gather language data //Gather language data
console.debug(`metrics/compute/${login}/plugins > languages > indepth > running linguist`) console.debug(`metrics/compute/${login}/plugins > languages > indepth > running linguist`)
const {files:{results:files}, languages:{results:languageResults}} = await linguist(path) const {files: {results: files}, languages: {results: languageResults}} = await linguist(path)
Object.assign(results.colors, Object.fromEntries(Object.entries(languageResults).map(([lang, {color}]) => [lang, color]))) Object.assign(results.colors, Object.fromEntries(Object.entries(languageResults).map(([lang, {color}]) => [lang, color])))
//Processing diff //Processing diff
@@ -207,18 +207,18 @@ async function analyze({login, imports, data}, {results, path, categories = ["pr
const edited = new Set() const edited = new Set()
console.debug(`metrics/compute/${login}/plugins > languages > indepth > checking git log`) console.debug(`metrics/compute/${login}/plugins > languages > indepth > checking git log`)
try { try {
await imports.run("git log --max-count=1", {cwd:path}) await imports.run("git log --max-count=1", {cwd: path})
} }
catch { catch {
console.debug(`metrics/compute/${login}/plugins > languages > indepth > repo seems empty or impossible to git log, skipping`) console.debug(`metrics/compute/${login}/plugins > languages > indepth > repo seems empty or impossible to git log, skipping`)
return return
} }
const pending = [] const pending = []
for (let page = 0; ; page++) { for (let page = 0;; page++) {
try { try {
console.debug(`metrics/compute/${login}/plugins > languages > indepth > processing commits ${page * per_page} from ${(page + 1) * per_page}`) console.debug(`metrics/compute/${login}/plugins > languages > indepth > processing commits ${page * per_page} from ${(page + 1) * per_page}`)
let empty = true, file = null, lang = null let empty = true, file = null, lang = null
await imports.spawn("git", ["log", ...data.shared["commits.authoring"].map(authoring => `--author="${authoring}"`), "--regexp-ignore-case", "--format=short", "--no-merges", "--patch", `--max-count=${per_page}`, `--skip=${page * per_page}`], {cwd:path}, { await imports.spawn("git", ["log", ...data.shared["commits.authoring"].map(authoring => `--author="${authoring}"`), "--regexp-ignore-case", "--format=short", "--no-merges", "--patch", `--max-count=${per_page}`, `--skip=${page * per_page}`], {cwd: path}, {
stdout(line) { stdout(line) {
try { try {
//Unflag empty output //Unflag empty output
@@ -230,7 +230,7 @@ async function analyze({login, imports, data}, {results, path, categories = ["pr
const sha = line.match(/[0-9a-f]{40}/)?.[0] const sha = line.match(/[0-9a-f]{40}/)?.[0]
if (sha) { if (sha) {
pending.push( pending.push(
imports.run(`git verify-commit ${sha}`, {cwd:path, env:{LANG:"en_GB"}}, {log:false, prefixed:false}) imports.run(`git verify-commit ${sha}`, {cwd: path, env: {LANG: "en_GB"}}, {log: false, prefixed: false})
.then(() => results.verified.signature++) .then(() => results.verified.signature++)
.catch(() => null), .catch(() => null),
) )
@@ -289,26 +289,26 @@ async function analyze({login, imports, data}, {results, path, categories = ["pr
//import.meta.main //import.meta.main
if (/languages.analyzers.mjs$/.test(process.argv[1])) { if (/languages.analyzers.mjs$/.test(process.argv[1])) {
(async function() { ;(async function() {
//Parse inputs //Parse inputs
const [_authoring, path] = process.argv.slice(2) const [_authoring, path] = process.argv.slice(2)
if ((!_authoring) || (!path)) { if ((!_authoring) || (!path)) {
console.log("Usage is:\n npm run indepth -- <commits authoring> <repository local path>\n\n") console.log("Usage is:\n npm run indepth -- <commits authoring> <repository local path>\n\n")
process.exit(1) process.exit(1)
} }
const {default:setup} = await import("../../app/metrics/setup.mjs") const {default: setup} = await import("../../app/metrics/setup.mjs")
const {conf:{metadata}} = await setup({log:false}) const {conf: {metadata}} = await setup({log: false})
const {"commits.authoring":authoring} = await metadata.plugins.base.inputs({q:{"commits.authoring":_authoring}, account:"bypass"}) const {"commits.authoring": authoring} = await metadata.plugins.base.inputs({q: {"commits.authoring": _authoring}, account: "bypass"})
const data = {shared:{"commits.authoring":authoring}} const data = {shared: {"commits.authoring": authoring}}
//Prepare call //Prepare call
const imports = await import("../../app/metrics/utils.mjs") const imports = await import("../../app/metrics/utils.mjs")
const results = {total:0, lines:{}, colors:{}, stats:{}, missed:{lines:0, bytes:0, commits:0}} const results = {total: 0, lines: {}, colors: {}, stats: {}, missed: {lines: 0, bytes: 0, commits: 0}}
console.debug = log => /exited with code null/.test(log) ? console.error(log.replace(/^.*--max-count=(?<step>\d+) --skip=(?<start>\d+).*$/, (_, step, start) => `error: skipped commits ${start} from ${Number(start) + Number(step)}`)) : null console.debug = log => /exited with code null/.test(log) ? console.error(log.replace(/^.*--max-count=(?<step>\d+) --skip=(?<start>\d+).*$/, (_, step, start) => `error: skipped commits ${start} from ${Number(start) + Number(step)}`)) : null
//Analyze repository //Analyze repository
console.log(`commits authoring | ${authoring}\nrepository path | ${path}\n`) console.log(`commits authoring | ${authoring}\nrepository path | ${path}\n`)
await analyze({login:"cli", data, imports}, {results, path}) await analyze({login: "cli", data, imports}, {results, path})
console.log(results) console.log(results)
})() })()
} }

View File

@@ -1,5 +1,5 @@
//Imports //Imports
import {indepth as indepth_analyzer, recent as recent_analyzer} from "./analyzers.mjs" import { indepth as indepth_analyzer, recent as recent_analyzer } from "./analyzers.mjs"
//Setup //Setup
export default async function({login, data, imports, q, rest, account}, {enabled = false, extras = false} = {}) { export default async function({login, data, imports, q, rest, account}, {enabled = false, extras = false} = {}) {
@@ -10,14 +10,14 @@ export default async function({login, data, imports, q, rest, account}, {enabled
return null return null
//Context //Context
let context = {mode:"user"} let context = {mode: "user"}
if (q.repo) { if (q.repo) {
console.debug(`metrics/compute/${login}/plugins > languages > switched to repository mode`) console.debug(`metrics/compute/${login}/plugins > languages > switched to repository mode`)
context = {...context, mode:"repository"} context = {...context, mode: "repository"}
} }
//Load inputs //Load inputs
let {ignored, skipped, other, colors, aliases, details, threshold, limit, indepth, "analysis.timeout":timeout, sections, categories, "recent.categories":_recent_categories, "recent.load":_recent_load, "recent.days":_recent_days} = imports.metadata.plugins.languages let {ignored, skipped, other, colors, aliases, details, threshold, limit, indepth, "analysis.timeout": timeout, sections, categories, "recent.categories": _recent_categories, "recent.load": _recent_load, "recent.days": _recent_days} = imports.metadata.plugins.languages
.inputs({ .inputs({
data, data,
account, account,
@@ -40,11 +40,11 @@ export default async function({login, data, imports, q, rest, account}, {enabled
//Unique languages //Unique languages
const repositories = [...data.user.repositories.nodes, ...data.user.repositoriesContributedTo.nodes] const repositories = [...data.user.repositories.nodes, ...data.user.repositoriesContributedTo.nodes]
const unique = new Set(repositories.flatMap(repository => repository.languages.edges.map(({node:{name}}) => name))).size const unique = new Set(repositories.flatMap(repository => repository.languages.edges.map(({node: {name}}) => name))).size
//Iterate through user's repositories and retrieve languages data //Iterate through user's repositories and retrieve languages data
console.debug(`metrics/compute/${login}/plugins > languages > processing ${data.user.repositories.nodes.length} repositories`) console.debug(`metrics/compute/${login}/plugins > languages > processing ${data.user.repositories.nodes.length} repositories`)
const languages = {unique, sections, details, indepth, colors:{}, total:0, stats:{}, "stats.recent":{}} const languages = {unique, sections, details, indepth, colors: {}, total: 0, stats: {}, "stats.recent": {}}
const customColors = {} const customColors = {}
for (const repository of data.user.repositories.nodes) { for (const repository of data.user.repositories.nodes) {
//Skip repository if asked //Skip repository if asked
@@ -53,7 +53,7 @@ export default async function({login, data, imports, q, rest, account}, {enabled
continue continue
} }
//Process repository languages //Process repository languages
for (const {size, node:{color, name}} of Object.values(repository.languages.edges)) { for (const {size, node: {color, name}} of Object.values(repository.languages.edges)) {
languages.stats[name] = (languages.stats[name] ?? 0) + size languages.stats[name] = (languages.stats[name] ?? 0) + size
if (colors[name.toLocaleLowerCase()]) if (colors[name.toLocaleLowerCase()])
customColors[name] = colors[name.toLocaleLowerCase()] customColors[name] = colors[name.toLocaleLowerCase()]
@@ -69,7 +69,7 @@ export default async function({login, data, imports, q, rest, account}, {enabled
if ((sections.includes("recently-used")) && (context.mode === "user")) { if ((sections.includes("recently-used")) && (context.mode === "user")) {
try { try {
console.debug(`metrics/compute/${login}/plugins > languages > using recent analyzer`) console.debug(`metrics/compute/${login}/plugins > languages > using recent analyzer`)
languages["stats.recent"] = await recent_analyzer({login, data, imports, rest, account}, {skipped, categories:_recent_categories ?? categories, days:_recent_days, load:_recent_load, timeout}) languages["stats.recent"] = await recent_analyzer({login, data, imports, rest, account}, {skipped, categories: _recent_categories ?? categories, days: _recent_days, load: _recent_load, timeout})
Object.assign(languages.colors, languages["stats.recent"].colors) Object.assign(languages.colors, languages["stats.recent"].colors)
} }
catch (error) { catch (error) {
@@ -83,8 +83,8 @@ export default async function({login, data, imports, q, rest, account}, {enabled
const gpg = [] const gpg = []
try { try {
for (const username of [login, "web-flow"]) { for (const username of [login, "web-flow"]) {
const {data:keys} = await rest.users.listGpgKeysForUser({username}) const {data: keys} = await rest.users.listGpgKeysForUser({username})
gpg.push(...keys.map(({key_id:id, raw_key:pub, emails}) => ({id, pub, emails}))) gpg.push(...keys.map(({key_id: id, raw_key: pub, emails}) => ({id, pub, emails})))
if (username === login) { if (username === login) {
for (const {email} of gpg.flatMap(({emails}) => emails)) { for (const {email} of gpg.flatMap(({emails}) => emails)) {
console.debug(`metrics/compute/${login}/plugins > languages > auto-adding ${email} to commits_authoring (fetched from gpg)`) console.debug(`metrics/compute/${login}/plugins > languages > auto-adding ${email} to commits_authoring (fetched from gpg)`)
@@ -126,9 +126,10 @@ export default async function({login, data, imports, q, rest, account}, {enabled
} }
//Compute languages stats //Compute languages stats
for (const {section, stats = {}, lines = {}, missed = {bytes:0}, total = 0} of [{section:"favorites", stats:languages.stats, lines:languages.lines, total:languages.total, missed:languages.missed}, {section:"recent", ...languages["stats.recent"]}]) { for (const {section, stats = {}, lines = {}, missed = {bytes: 0}, total = 0} of [{section: "favorites", stats: languages.stats, lines: languages.lines, total: languages.total, missed: languages.missed}, {section: "recent", ...languages["stats.recent"]}]) {
console.debug(`metrics/compute/${login}/plugins > languages > computing stats ${section}`) console.debug(`metrics/compute/${login}/plugins > languages > computing stats ${section}`)
languages[section] = Object.entries(stats).filter(([name]) => !ignored.includes(name.toLocaleLowerCase())).sort(([_an, a], [_bn, b]) => b - a).slice(0, limit).map(([name, value]) => ({name, value, size:value, color:languages.colors[name], x:0})).filter(({value}) => value / total > threshold languages[section] = Object.entries(stats).filter(([name]) => !ignored.includes(name.toLocaleLowerCase())).sort(([_an, a], [_bn, b]) => b - a).slice(0, limit).map(([name, value]) => ({name, value, size: value, color: languages.colors[name], x: 0})).filter(({value}) =>
value / total > threshold
) )
if (other) { if (other) {
let value = indepth ? missed.bytes : Object.entries(stats).filter(([name]) => !Object.values(languages[section]).map(({name}) => name).includes(name)).reduce((a, [_, b]) => a + b, 0) let value = indepth ? missed.bytes : Object.entries(stats).filter(([name]) => !Object.values(languages[section]).map(({name}) => name).includes(name)).reduce((a, [_, b]) => a + b, 0)
@@ -141,7 +142,7 @@ export default async function({login, data, imports, q, rest, account}, {enabled
languages[section].push({name:"Other", value, size:value, get lines() { return missed.lines }, set lines(_) { }, x:0}) //eslint-disable-line brace-style, no-empty-function, max-statements-per-line languages[section].push({name:"Other", value, size:value, get lines() { return missed.lines }, set lines(_) { }, x:0}) //eslint-disable-line brace-style, no-empty-function, max-statements-per-line
} }
} }
const visible = {total:Object.values(languages[section]).map(({size}) => size).reduce((a, b) => a + b, 0)} const visible = {total: Object.values(languages[section]).map(({size}) => size).reduce((a, b) => a + b, 0)}
for (let i = 0; i < languages[section].length; i++) { for (let i = 0; i < languages[section].length; i++) {
const {name} = languages[section][i] const {name} = languages[section][i]
languages[section][i].value /= visible.total languages[section][i].value /= visible.total
@@ -159,6 +160,6 @@ export default async function({login, data, imports, q, rest, account}, {enabled
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -10,8 +10,8 @@ export default async function({login, q, imports, data, graphql, queries, accoun
let {setup, ratio, legal} = imports.metadata.plugins.licenses.inputs({data, account, q}) let {setup, ratio, legal} = imports.metadata.plugins.licenses.inputs({data, account, q})
//Initialization //Initialization
const {user:{repository}} = await graphql(queries.licenses.repository({owner:data.repo.owner.login, name:data.repo.name, account})) const {user: {repository}} = await graphql(queries.licenses.repository({owner: data.repo.owner.login, name: data.repo.name, account}))
const result = {ratio, legal, default:repository.licenseInfo, licensed:{available:false}, text:{}, list:[], used:{}, dependencies:[], known:0, unknown:0} const result = {ratio, legal, default: repository.licenseInfo, licensed: {available: false}, text: {}, list: [], used: {}, dependencies: [], known: 0, unknown: 0}
const {used, text} = result const {used, text} = result
//Register existing licenses properties //Register existing licenses properties
@@ -29,8 +29,8 @@ export default async function({login, q, imports, data, graphql, queries, accoun
const path = imports.paths.join(imports.os.tmpdir(), `${repository.databaseId}`) const path = imports.paths.join(imports.os.tmpdir(), `${repository.databaseId}`)
//Create temporary directory //Create temporary directory
console.debug(`metrics/compute/${login}/plugins > licenses > creating temp dir ${path}`) console.debug(`metrics/compute/${login}/plugins > licenses > creating temp dir ${path}`)
await imports.fs.rm(path, {recursive:true, force:true}) await imports.fs.rm(path, {recursive: true, force: true})
await imports.fs.mkdir(path, {recursive:true}) await imports.fs.mkdir(path, {recursive: true})
//Clone repository //Clone repository
console.debug(`metrics/compute/${login}/plugins > licenses > cloning temp git repository ${repository.url} to ${path}`) console.debug(`metrics/compute/${login}/plugins > licenses > cloning temp git repository ${repository.url} to ${path}`)
const git = imports.git(path) const git = imports.git(path)
@@ -38,7 +38,7 @@ export default async function({login, q, imports, data, graphql, queries, accoun
//Run setup //Run setup
if (setup) { if (setup) {
console.debug(`metrics/compute/${login}/plugins > licenses > running setup [${setup}]`) console.debug(`metrics/compute/${login}/plugins > licenses > running setup [${setup}]`)
await imports.run(setup, {cwd:path}, {prefixed:false}) await imports.run(setup, {cwd: path}, {prefixed: false})
} }
//Create configuration file if needed //Create configuration file if needed
if (!(await imports.fs.stat(imports.paths.join(path, ".licensed.yml")).then(() => 1).catch(() => 0))) { if (!(await imports.fs.stat(imports.paths.join(path, ".licensed.yml")).then(() => 1).catch(() => 0))) {
@@ -50,33 +50,35 @@ export default async function({login, q, imports, data, graphql, queries, accoun
].join("\n"), ].join("\n"),
) )
} }
else else {
console.debug(`metrics/compute/${login}/plugins > licenses > a .licensed.yml configuration file already exists`) console.debug(`metrics/compute/${login}/plugins > licenses > a .licensed.yml configuration file already exists`)
}
//Spawn licensed process //Spawn licensed process
console.debug(`metrics/compute/${login}/plugins > licenses > running licensed`) console.debug(`metrics/compute/${login}/plugins > licenses > running licensed`)
JSON.parse(await imports.run("licensed list --format=json --licenses", {cwd:path})).apps JSON.parse(await imports.run("licensed list --format=json --licenses", {cwd: path})).apps
.map(({sources}) => sources?.flatMap(source => source.dependencies?.map(({dependency, license}) => { .map(({sources}) =>
sources?.flatMap(source =>
source.dependencies?.map(({dependency, license}) => {
used[license] = (used[license] ?? 0) + 1 used[license] = (used[license] ?? 0) + 1
result.dependencies.push(dependency) result.dependencies.push(dependency)
result.known += (license in licenses) result.known += license in licenses
result.unknown += !(license in licenses) result.unknown += !(license in licenses)
}) })
) )
) )
//Cleaning //Cleaning
console.debug(`metrics/compute/${login}/plugins > licensed > cleaning temp dir ${path}`) console.debug(`metrics/compute/${login}/plugins > licensed > cleaning temp dir ${path}`)
await imports.fs.rm(path, {recursive:true, force:true}) await imports.fs.rm(path, {recursive: true, force: true})
} }
else else {
console.debug(`metrics/compute/${login}/plugins > licenses > licensed not available`) console.debug(`metrics/compute/${login}/plugins > licenses > licensed not available`)
}
//List licenses properties //List licenses properties
console.debug(`metrics/compute/${login}/plugins > licenses > compute licenses properties`) console.debug(`metrics/compute/${login}/plugins > licenses > compute licenses properties`)
const base = {permissions:new Set(), limitations:new Set(), conditions:new Set()} const base = {permissions: new Set(), limitations: new Set(), conditions: new Set()}
const combined = {permissions:new Set(), limitations:new Set(), conditions:new Set()} const combined = {permissions: new Set(), limitations: new Set(), conditions: new Set()}
const detected = Object.entries(used).map(([key, _value]) => ({key})) const detected = Object.entries(used).map(([key, _value]) => ({key}))
for (const properties of Object.keys(base)) { for (const properties of Object.keys(base)) {
//Base license //Base license
@@ -89,15 +91,15 @@ export default async function({login, q, imports, data, graphql, queries, accoun
//Merge limitations and conditions //Merge limitations and conditions
for (const properties of ["limitations", "conditions"]) for (const properties of ["limitations", "conditions"])
result[properties] = [[...base[properties]].map(key => ({key, text:text[key], inherited:false})), [...combined[properties]].filter(key => !base[properties].has(key)).map(key => ({key, text:text[key], inherited:true}))].flat() result[properties] = [[...base[properties]].map(key => ({key, text: text[key], inherited: false})), [...combined[properties]].filter(key => !base[properties].has(key)).map(key => ({key, text: text[key], inherited: true}))].flat()
//Remove base permissions conflicting with inherited limitations //Remove base permissions conflicting with inherited limitations
result.permissions = [...base.permissions].filter(key => !combined.limitations.has(key)).map(key => ({key, text:text[key]})) result.permissions = [...base.permissions].filter(key => !combined.limitations.has(key)).map(key => ({key, text: text[key]}))
//Count used licenses //Count used licenses
console.debug(`metrics/compute/${login}/plugins > licenses > computing ratio`) console.debug(`metrics/compute/${login}/plugins > licenses > computing ratio`)
const total = Object.values(used).reduce((a, b) => a + b, 0) const total = Object.values(used).reduce((a, b) => a + b, 0)
//Format used licenses and compute positions //Format used licenses and compute positions
const list = Object.entries(used).map(([key, count]) => ({name:licenses[key]?.spdxId ?? `${key.charAt(0).toLocaleUpperCase()}${key.substring(1)}`, key, count, value:count / total, x:0, color:licenses[key]?.color ?? "#6e7681", order:licenses[key]?.order ?? -1})).sort(( const list = Object.entries(used).map(([key, count]) => ({name: licenses[key]?.spdxId ?? `${key.charAt(0).toLocaleUpperCase()}${key.substring(1)}`, key, count, value: count / total, x: 0, color: licenses[key]?.color ?? "#6e7681", order: licenses[key]?.order ?? -1})).sort((
a, a,
b, b,
) => a.order === b.order ? b.count - a.count : b.order - a.order) ) => a.order === b.order ? b.count - a.count : b.order - a.order)
@@ -111,7 +113,7 @@ export default async function({login, q, imports, data, graphql, queries, accoun
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -11,24 +11,25 @@ export default async function({login, data, imports, rest, q, account}, {enabled
skipped.push(...data.shared["repositories.skipped"]) skipped.push(...data.shared["repositories.skipped"])
//Context //Context
let context = {mode:"user"} let context = {mode: "user"}
if (q.repo) { if (q.repo) {
console.debug(`metrics/compute/${login}/plugins > people > switched to repository mode`) console.debug(`metrics/compute/${login}/plugins > people > switched to repository mode`)
context = {...context, mode:"repository"} context = {...context, mode: "repository"}
} }
//Repositories //Repositories
const repositories = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})) ?? [] const repositories = data.user.repositories.nodes.map(({name: repo, owner: {login: owner}}) => ({repo, owner})) ?? []
//Get contributors stats from repositories //Get contributors stats from repositories
console.debug(`metrics/compute/${login}/plugins > lines > querying api`) console.debug(`metrics/compute/${login}/plugins > lines > querying api`)
const lines = {added:0, deleted:0, changed:0} const lines = {added: 0, deleted: 0, changed: 0}
const response = [...await Promise.allSettled(repositories.map(({repo, owner}) => (skipped.includes(repo.toLocaleLowerCase())) || (skipped.includes(`${owner}/${repo}`.toLocaleLowerCase())) ? {} : rest.repos.getContributorsStats({owner, repo})))].filter(({status}) => status === "fulfilled" const response = [...await Promise.allSettled(repositories.map(({repo, owner}) => (skipped.includes(repo.toLocaleLowerCase())) || (skipped.includes(`${owner}/${repo}`.toLocaleLowerCase())) ? {} : rest.repos.getContributorsStats({owner, repo})))].filter(({status}) =>
status === "fulfilled"
).map(({value}) => value) ).map(({value}) => value)
//Compute changed lines //Compute changed lines
console.debug(`metrics/compute/${login}/plugins > lines > computing total diff`) console.debug(`metrics/compute/${login}/plugins > lines > computing total diff`)
response.map(({data:repository}) => { response.map(({data: repository}) => {
//Check if data are available //Check if data are available
if (!Array.isArray(repository)) if (!Array.isArray(repository))
return return
@@ -43,6 +44,6 @@ export default async function({login, data, imports, rest, q, account}, {enabled
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -3,28 +3,28 @@ import crypto from "crypto"
//Supported providers //Supported providers
const providers = { const providers = {
apple:{ apple: {
name:"Apple Music", name: "Apple Music",
embed:/^https:..embed.music.apple.com.\w+.playlist/, embed: /^https:..embed.music.apple.com.\w+.playlist/,
}, },
spotify:{ spotify: {
name:"Spotify", name: "Spotify",
embed:/^https:..open.spotify.com.embed.playlist/, embed: /^https:..open.spotify.com.embed.playlist/,
}, },
lastfm:{ lastfm: {
name:"Last.fm", name: "Last.fm",
embed:/^\b$/, embed: /^\b$/,
}, },
youtube:{ youtube: {
name:"YouTube Music", name: "YouTube Music",
embed:/^https:..music.youtube.com.playlist/, embed: /^https:..music.youtube.com.playlist/,
}, },
} }
//Supported modes //Supported modes
const modes = { const modes = {
playlist:"Suggested tracks", playlist: "Suggested tracks",
recent:"Recently played", recent: "Recently played",
top:"Top played", top: "Top played",
} }
//Setup //Setup
@@ -47,7 +47,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
let tracks = null let tracks = null
//Load inputs //Load inputs
let {provider, mode, playlist, limit, user, "played.at":played_at, "time.range":time_range, "top.type":top_type, token:_token} = imports.metadata.plugins.music.inputs({data, account, q}) let {provider, mode, playlist, limit, user, "played.at": played_at, "time.range": time_range, "top.type": top_type, token: _token} = imports.metadata.plugins.music.inputs({data, account, q})
if ((sandbox) && (_token)) { if ((sandbox) && (_token)) {
token = _token token = _token
console.debug(`metrics/compute/${login}/plugins > music > overriden token value through user inputs as sandbox mode is enabled`) console.debug(`metrics/compute/${login}/plugins > music > overriden token value through user inputs as sandbox mode is enabled`)
@@ -71,16 +71,16 @@ export default async function({login, imports, data, q, account}, {enabled = fal
} }
//Provider //Provider
if (!(provider in providers)) if (!(provider in providers))
throw {error:{message:provider ? `Unsupported provider "${provider}"` : "Missing provider"}, ...raw} throw {error: {message: provider ? `Unsupported provider "${provider}"` : "Missing provider"}, ...raw}
//Mode //Mode
if (!(mode in modes)) if (!(mode in modes))
throw {error:{message:`Unsupported mode "${mode}"`}, ...raw} throw {error: {message: `Unsupported mode "${mode}"`}, ...raw}
//Playlist mode //Playlist mode
if (mode === "playlist") { if (mode === "playlist") {
if (!playlist) if (!playlist)
throw {error:{message:"Missing playlist url"}, ...raw} throw {error: {message: "Missing playlist url"}, ...raw}
if (!providers[provider].embed.test(playlist)) if (!providers[provider].embed.test(playlist))
throw {error:{message:"Unsupported playlist url format"}, ...raw} throw {error: {message: "Unsupported playlist url format"}, ...raw}
} }
//Limit //Limit
limit = Math.max(1, Math.min(100, Number(limit))) limit = Math.max(1, Math.min(100, Number(limit)))
@@ -111,9 +111,9 @@ export default async function({login, imports, data, q, account}, {enabled = fal
...await frame.evaluate(() => { ...await frame.evaluate(() => {
const tracklist = document.querySelector("embed-root").shadowRoot.querySelector(".audio-tracklist") const tracklist = document.querySelector("embed-root").shadowRoot.querySelector(".audio-tracklist")
return [...tracklist.querySelectorAll("embed-audio-tracklist-item")].map(item => ({ return [...tracklist.querySelectorAll("embed-audio-tracklist-item")].map(item => ({
name:item.querySelector(".audio-tracklist-item__metadata h3").innerText, name: item.querySelector(".audio-tracklist-item__metadata h3").innerText,
artist:item.querySelector(".audio-tracklist-item__metadata h4").innerText, artist: item.querySelector(".audio-tracklist-item__metadata h4").innerText,
artwork:item.querySelector("apple-music-artwork")?.shadowRoot?.querySelector("picture source")?.srcset?.split(",")?.[0]?.replace(/\s+\d+x$/, ""), artwork: item.querySelector("apple-music-artwork")?.shadowRoot?.querySelector("picture source")?.srcset?.split(",")?.[0]?.replace(/\s+\d+x$/, ""),
})) }))
}), }),
] ]
@@ -124,11 +124,12 @@ export default async function({login, imports, data, q, account}, {enabled = fal
//Parse tracklist //Parse tracklist
await frame.waitForSelector("table") await frame.waitForSelector("table")
tracks = [ tracks = [
...await frame.evaluate(() => [...document.querySelectorAll("table tr")].map(tr => ({ ...await frame.evaluate(() =>
name:tr.querySelector("td:nth-child(2) div div:nth-child(1)").innerText, [...document.querySelectorAll("table tr")].map(tr => ({
artist:tr.querySelector("td:nth-child(2) div div:nth-child(2)").innerText, name: tr.querySelector("td:nth-child(2) div div:nth-child(1)").innerText,
artist: tr.querySelector("td:nth-child(2) div div:nth-child(2)").innerText,
//Spotify doesn't provide artworks so we fallback on playlist artwork instead //Spotify doesn't provide artworks so we fallback on playlist artwork instead
artwork:window.getComputedStyle(document.querySelector("button[title=Play]")?.parentNode ?? document.querySelector("button").parentNode, null).backgroundImage.match(/^url\("(?<url>https:...+)"\)$/)?.groups?.url ?? null, artwork: window.getComputedStyle(document.querySelector("button[title=Play]")?.parentNode ?? document.querySelector("button").parentNode, null).backgroundImage.match(/^url\("(?<url>https:...+)"\)$/)?.groups?.url ?? null,
})) }))
), ),
] ]
@@ -140,10 +141,11 @@ export default async function({login, imports, data, q, account}, {enabled = fal
await frame.evaluate(() => window.scrollBy(0, window.innerHeight)) await frame.evaluate(() => window.scrollBy(0, window.innerHeight))
//Parse tracklist //Parse tracklist
tracks = [ tracks = [
...await frame.evaluate(() => [...document.querySelectorAll("ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer")].map(item => ({ ...await frame.evaluate(() =>
name:item.querySelector("yt-formatted-string.title > a")?.innerText ?? "", [...document.querySelectorAll("ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer")].map(item => ({
artist:item.querySelector(".secondary-flex-columns > yt-formatted-string > a")?.innerText ?? "", name: item.querySelector("yt-formatted-string.title > a")?.innerText ?? "",
artwork:item.querySelector("img").src, artist: item.querySelector(".secondary-flex-columns > yt-formatted-string > a")?.innerText ?? "",
artwork: item.querySelector("img").src,
})) }))
), ),
] ]
@@ -151,7 +153,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
} }
//Unsupported //Unsupported
default: default:
throw {error:{message:`Unsupported mode "${mode}" for provider "${provider}"`}, ...raw} throw {error: {message: `Unsupported mode "${mode}" for provider "${provider}"`}, ...raw}
} }
//Close browser //Close browser
console.debug(`metrics/compute/${login}/plugins > music > closing browser`) console.debug(`metrics/compute/${login}/plugins > music > closing browser`)
@@ -160,7 +162,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
if (Array.isArray(tracks)) { if (Array.isArray(tracks)) {
//Tracks //Tracks
console.debug(`metrics/compute/${login}/plugins > music > found ${tracks.length} tracks`) console.debug(`metrics/compute/${login}/plugins > music > found ${tracks.length} tracks`)
console.debug(imports.util.inspect(tracks, {depth:Infinity, maxStringLength:256})) console.debug(imports.util.inspect(tracks, {depth: Infinity, maxStringLength: 256}))
//Shuffle tracks //Shuffle tracks
tracks = imports.shuffle(tracks) tracks = imports.shuffle(tracks)
} }
@@ -175,14 +177,14 @@ export default async function({login, imports, data, q, account}, {enabled = fal
//Prepare credentials //Prepare credentials
const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim()) const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim())
if ((!client_id) || (!client_secret) || (!refresh_token)) if ((!client_id) || (!client_secret) || (!refresh_token))
throw {error:{message:"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 //API call and parse tracklist
try { try {
//Request access token //Request access token
console.debug(`metrics/compute/${login}/plugins > music > requesting access token with spotify refresh token`) 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})}`, { 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:{ headers: {
"Content-Type":"application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
}) })
console.debug(`metrics/compute/${login}/plugins > music > got access token`) console.debug(`metrics/compute/${login}/plugins > music > got access token`)
@@ -193,16 +195,16 @@ export default async function({login, imports, data, q, account}, {enabled = fal
//Load track half-hour by half-hour //Load track half-hour by half-hour
const timestamp = Date.now() - hours * 60 * 60 * 1000 const timestamp = Date.now() - hours * 60 * 60 * 1000
const loaded = (await imports.axios.get(`https://api.spotify.com/v1/me/player/recently-played?after=${timestamp}`, { const loaded = (await imports.axios.get(`https://api.spotify.com/v1/me/player/recently-played?after=${timestamp}`, {
headers:{ headers: {
"Content-Type":"application/json", "Content-Type": "application/json",
Accept:"application/json", Accept: "application/json",
Authorization:`Bearer ${access}`, Authorization: `Bearer ${access}`,
}, },
})).data.items.map(({track, played_at}) => ({ })).data.items.map(({track, played_at}) => ({
name:track.name, name: track.name,
artist:track.artists[0].name, artist: track.artists[0].name,
artwork:track.album.images[0].url, artwork: track.album.images[0].url,
played_at:played_at ? `${imports.format.date(played_at, {time:true})} on ${imports.format.date(played_at, {date:true})}` : null, played_at: played_at ? `${imports.format.date(played_at, {time: true})} on ${imports.format.date(played_at, {date: true})}` : null,
})) }))
//Ensure no duplicate are added //Ensure no duplicate are added
for (const track of loaded) { for (const track of loaded) {
@@ -221,7 +223,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
const description = error.response.data?.error_description ?? null const description = error.response.data?.error_description ?? null
const message = `API returned ${status}${description ? ` (${description})` : ""}` const message = `API returned ${status}${description ? ` (${description})` : ""}`
error = error.response?.data ?? null error = error.response?.data ?? null
throw {error:{message, instance:error}, ...raw} throw {error: {message, instance: error}, ...raw}
} }
throw error throw error
} }
@@ -233,14 +235,14 @@ export default async function({login, imports, data, q, account}, {enabled = fal
try { try {
console.debug(`metrics/compute/${login}/plugins > music > querying lastfm api`) console.debug(`metrics/compute/${login}/plugins > music > querying lastfm api`)
tracks = (await imports.axios.get(`https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${user}&api_key=${token}&limit=${limit}&format=json`, { tracks = (await imports.axios.get(`https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${user}&api_key=${token}&limit=${limit}&format=json`, {
headers:{ headers: {
"User-Agent":"lowlighter/metrics", "User-Agent": "lowlighter/metrics",
Accept:"application/json", Accept: "application/json",
}, },
})).data.recenttracks.track.map(track => ({ })).data.recenttracks.track.map(track => ({
name:track.name, name: track.name,
artist:track.artist["#text"], artist: track.artist["#text"],
artwork:track.image.reverse()[0]["#text"], artwork: track.image.reverse()[0]["#text"],
})) }))
} }
//Handle errors //Handle errors
@@ -250,7 +252,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
const description = error.response.data?.message ?? null const description = error.response.data?.message ?? null
const message = `API returned ${status}${description ? ` (${description})` : ""}` const message = `API returned ${status}${description ? ` (${description})` : ""}`
error = error.response?.data ?? null error = error.response?.data ?? null
throw {error:{message, instance:error}, ...raw} throw {error: {message, instance: error}, ...raw}
} }
throw error throw error
} }
@@ -267,25 +269,25 @@ export default async function({login, imports, data, q, account}, {enabled = fal
//Request access token //Request access token
console.debug(`metrics/compute/${login}/plugins > music > requesting access token with youtube refresh token`) console.debug(`metrics/compute/${login}/plugins > music > requesting access token with youtube refresh token`)
const res = await imports.axios.post("https://music.youtube.com/youtubei/v1/browse?alt=json&key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30", { const res = await imports.axios.post("https://music.youtube.com/youtubei/v1/browse?alt=json&key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30", {
browseEndpointContextSupportedConfigs:{ browseEndpointContextSupportedConfigs: {
browseEndpointContextMusicConfig:{ browseEndpointContextMusicConfig: {
pageType:"MUSIC_PAGE_TYPE_PLAYLIST", pageType: "MUSIC_PAGE_TYPE_PLAYLIST",
}, },
}, },
context:{ context: {
client:{ client: {
clientName:"WEB_REMIX", clientName: "WEB_REMIX",
clientVersion:"1.20211129.00.01", clientVersion: "1.20211129.00.01",
gl:"US", gl: "US",
hl:"en", hl: "en",
}, },
}, },
browseId:"FEmusic_history", browseId: "FEmusic_history",
}, { }, {
headers:{ headers: {
Authorization:SAPISIDHASH, Authorization: SAPISIDHASH,
Cookie:token, Cookie: token,
"x-origin":"https://music.youtube.com", "x-origin": "https://music.youtube.com",
}, },
}) })
//Retrieve tracks //Retrieve tracks
@@ -296,9 +298,9 @@ export default async function({login, imports, data, q, account}, {enabled = fal
for (let i = 0; i < parsedHistory.length; i++) { for (let i = 0; i < parsedHistory.length; i++) {
let track = parsedHistory[i] let track = parsedHistory[i]
tracks.push({ tracks.push({
name:track.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text, name: track.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text,
artist:track.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text, artist: track.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text,
artwork:track.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url, artwork: track.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url,
}) })
//Early break //Early break
if (tracks.length >= limit) if (tracks.length >= limit)
@@ -312,7 +314,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
const description = error.response.data?.error_description ?? null const description = error.response.data?.error_description ?? null
const message = `API returned ${status}${description ? ` (${description})` : ""}` const message = `API returned ${status}${description ? ` (${description})` : ""}`
error = error.response?.data ?? null error = error.response?.data ?? null
throw {error:{message, instance:error}, ...raw} throw {error: {message, instance: error}, ...raw}
} }
throw error throw error
} }
@@ -320,7 +322,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
} }
//Unsupported //Unsupported
default: default:
throw {error:{message:`Unsupported mode "${mode}" for provider "${provider}"`}, ...raw} throw {error: {message: `Unsupported mode "${mode}" for provider "${provider}"`}, ...raw}
} }
break break
} }
@@ -337,7 +339,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
time_msg = "overall" time_msg = "overall"
break break
default: default:
throw {error:{message:`Unsupported time range "${time_range}"`}, ...raw} throw {error: {message: `Unsupported time range "${time_range}"`}, ...raw}
} }
if (top_type === "artists") { if (top_type === "artists") {
@@ -362,17 +364,17 @@ export default async function({login, imports, data, q, account}, {enabled = fal
//Prepare credentials //Prepare credentials
const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim()) const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim())
if ((!client_id) || (!client_secret) || (!refresh_token)) if ((!client_id) || (!client_secret) || (!refresh_token))
throw {error:{message:"Spotify token must contain client id/secret and refresh token"}} throw {error: {message: "Spotify token must contain client id/secret and refresh token"}}
else if (limit > 50) else if (limit > 50)
throw {error:{message:"Spotify top limit cannot be greater than 50"}} throw {error: {message: "Spotify top limit cannot be greater than 50"}}
//API call and parse tracklist //API call and parse tracklist
try { try {
//Request access token //Request access token
console.debug(`metrics/compute/${login}/plugins > music > requesting access token with spotify refresh token`) 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})}`, { 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:{ headers: {
"Content-Type":"application/x-www-form-urlencoded", "Content-Type": "application/x-www-form-urlencoded",
}, },
}) })
console.debug(`metrics/compute/${login}/plugins > music > got access token`) console.debug(`metrics/compute/${login}/plugins > music > got access token`)
@@ -384,33 +386,33 @@ export default async function({login, imports, data, q, account}, {enabled = fal
await imports.axios.get( await imports.axios.get(
`https://api.spotify.com/v1/me/top/artists?time_range=${time_range}_term&limit=${limit}`, `https://api.spotify.com/v1/me/top/artists?time_range=${time_range}_term&limit=${limit}`,
{ {
headers:{ headers: {
"Content-Type":"application/json", "Content-Type": "application/json",
Accept:"application/json", Accept: "application/json",
Authorization:`Bearer ${access}`, Authorization: `Bearer ${access}`,
}, },
}, },
) )
).data.items.map(({name, genres, images}) => ({ ).data.items.map(({name, genres, images}) => ({
name, name,
artist:genres.join(" • "), artist: genres.join(" • "),
artwork:images[0].url, artwork: images[0].url,
})) }))
: ( : (
await imports.axios.get( await imports.axios.get(
`https://api.spotify.com/v1/me/top/tracks?time_range=${time_range}_term&limit=${limit}`, `https://api.spotify.com/v1/me/top/tracks?time_range=${time_range}_term&limit=${limit}`,
{ {
headers:{ headers: {
"Content-Type":"application/json", "Content-Type": "application/json",
Accept:"application/json", Accept: "application/json",
Authorization:`Bearer ${access}`, Authorization: `Bearer ${access}`,
}, },
}, },
) )
).data.items.map(({name, artists, album}) => ({ ).data.items.map(({name, artists, album}) => ({
name, name,
artist:artists[0].name, artist: artists[0].name,
artwork:album.images[0].url, artwork: album.images[0].url,
})) }))
//Ensure no duplicate are added //Ensure no duplicate are added
for (const track of loaded) { for (const track of loaded) {
@@ -425,7 +427,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
const description = error.response.data?.error_description ?? null const description = error.response.data?.error_description ?? null
const message = `API returned ${status}${description ? ` (${description})` : ""}` const message = `API returned ${status}${description ? ` (${description})` : ""}`
error = error.response?.data ?? null error = error.response?.data ?? null
throw {error:{message, instance:error}, ...raw} throw {error: {message, instance: error}, ...raw}
} }
throw error throw error
} }
@@ -442,31 +444,31 @@ export default async function({login, imports, data, q, account}, {enabled = fal
await imports.axios.get( await imports.axios.get(
`https://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=${user}&api_key=${token}&limit=${limit}&period=${period}&format=json`, `https://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=${user}&api_key=${token}&limit=${limit}&period=${period}&format=json`,
{ {
headers:{ headers: {
"User-Agent":"lowlighter/metrics", "User-Agent": "lowlighter/metrics",
Accept:"application/json", Accept: "application/json",
}, },
}, },
) )
).data.topartists.artist.map(artist => ({ ).data.topartists.artist.map(artist => ({
name:artist.name, name: artist.name,
artist:`Play count: ${artist.playcount}`, artist: `Play count: ${artist.playcount}`,
artwork:artist.image.reverse()[0]["#text"], artwork: artist.image.reverse()[0]["#text"],
})) }))
: ( : (
await imports.axios.get( await imports.axios.get(
`https://ws.audioscrobbler.com/2.0/?method=user.gettoptracks&user=${user}&api_key=${token}&limit=${limit}&period=${period}&format=json`, `https://ws.audioscrobbler.com/2.0/?method=user.gettoptracks&user=${user}&api_key=${token}&limit=${limit}&period=${period}&format=json`,
{ {
headers:{ headers: {
"User-Agent":"lowlighter/metrics", "User-Agent": "lowlighter/metrics",
Accept:"application/json", Accept: "application/json",
}, },
}, },
) )
).data.toptracks.track.map(track => ({ ).data.toptracks.track.map(track => ({
name:track.name, name: track.name,
artist:track.artist.name, artist: track.artist.name,
artwork:track.image.reverse()[0]["#text"], artwork: track.image.reverse()[0]["#text"],
})) }))
} }
//Handle errors //Handle errors
@@ -476,7 +478,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
const description = error.response.data?.message ?? null const description = error.response.data?.message ?? null
const message = `API returned ${status}${description ? ` (${description})` : ""}` const message = `API returned ${status}${description ? ` (${description})` : ""}`
error = error.response?.data ?? null error = error.response?.data ?? null
throw {error:{message, instance:error}, ...raw} throw {error: {message, instance: error}, ...raw}
} }
throw error throw error
} }
@@ -484,13 +486,13 @@ export default async function({login, imports, data, q, account}, {enabled = fal
} }
//Unsupported //Unsupported
default: default:
throw {error:{message:`Unsupported mode "${mode}" for provider "${provider}"`}, ...raw} throw {error: {message: `Unsupported mode "${mode}" for provider "${provider}"`}, ...raw}
} }
break break
} }
//Unsupported //Unsupported
default: default:
throw {error:{message:`Unsupported mode "${mode}"`}, ...raw} throw {error: {message: `Unsupported mode "${mode}"`}, ...raw}
} }
//Format tracks //Format tracks
@@ -511,13 +513,13 @@ export default async function({login, imports, data, q, account}, {enabled = fal
} }
//Unhandled error //Unhandled error
throw {error:{message:"An error occured (could not retrieve tracks)"}} throw {error: {message: "An error occured (could not retrieve tracks)"}}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -17,19 +17,20 @@ export default async function({login, q, imports, rest, graphql, data, account,
let pushed = 0 let pushed = 0
do { do {
console.debug(`metrics/compute/${login}/plugins > notable > retrieving contributed repositories after ${cursor}`) console.debug(`metrics/compute/${login}/plugins > notable > retrieving contributed repositories after ${cursor}`)
const {user:{repositoriesContributedTo:{edges}}} = await graphql(queries.notable.contributions({login, types:types.map(x => x.toLocaleUpperCase()).join(", "), after:cursor ? `after: "${cursor}"` : "", repositories:data.shared["repositories.batch"] || 100})) const {user: {repositoriesContributedTo: {edges}}} = await graphql(queries.notable.contributions({login, types: types.map(x => x.toLocaleUpperCase()).join(", "), after: cursor ? `after: "${cursor}"` : "", repositories: data.shared["repositories.batch"] || 100}))
cursor = edges?.[edges?.length - 1]?.cursor cursor = edges?.[edges?.length - 1]?.cursor
edges edges
.filter(({node}) => !((skipped.includes(node.nameWithOwner.toLocaleLowerCase())) || (skipped.includes(node.nameWithOwner.split("/")[1].toLocaleLowerCase())))) .filter(({node}) => !((skipped.includes(node.nameWithOwner.toLocaleLowerCase())) || (skipped.includes(node.nameWithOwner.split("/")[1].toLocaleLowerCase()))))
.filter(({node}) => ({all:true, organization:node.isInOrganization, user:!node.isInOrganization}[from])) .filter(({node}) => ({all: true, organization: node.isInOrganization, user: !node.isInOrganization}[from]))
.filter(({node}) => imports.ghfilter(filter, {name:node.nameWithOwner, user:node.owner.login, stars:node.stargazers.totalCount, watchers:node.watchers.totalCount, forks:node.forks.totalCount})) .filter(({node}) => imports.ghfilter(filter, {name: node.nameWithOwner, user: node.owner.login, stars: node.stargazers.totalCount, watchers: node.watchers.totalCount, forks: node.forks.totalCount}))
.map(({node}) => contributions.push({handle:node.nameWithOwner, stars:node.stargazers.totalCount, issues:node.issues.totalCount, pulls:node.pullRequests.totalCount, organization:node.isInOrganization, avatarUrl:node.owner.avatarUrl})) .map(({node}) => contributions.push({handle: node.nameWithOwner, stars: node.stargazers.totalCount, issues: node.issues.totalCount, pulls: node.pullRequests.totalCount, organization: node.isInOrganization, avatarUrl: node.owner.avatarUrl}))
pushed = edges.length pushed = edges.length
} while ((pushed) && (cursor)) } while ((pushed) && (cursor))
} }
//Set contributions //Set contributions
contributions = (await Promise.all(contributions.map(async ({handle, stars, issues, pulls, avatarUrl, organization}) => ({name:handle.split("/").shift(), handle, stars, issues, pulls, avatar:await imports.imgb64(avatarUrl), organization})))).sort((a, b) => a.name.localeCompare(b.name) contributions = (await Promise.all(contributions.map(async ({handle, stars, issues, pulls, avatarUrl, organization}) => ({name: handle.split("/").shift(), handle, stars, issues, pulls, avatar: await imports.imgb64(avatarUrl), organization})))).sort((a, b) =>
a.name.localeCompare(b.name)
) )
console.debug(`metrics/compute/${login}/plugins > notable > found ${contributions.length} notable contributions`) console.debug(`metrics/compute/${login}/plugins > notable > found ${contributions.length} notable contributions`)
@@ -46,9 +47,9 @@ export default async function({login, q, imports, rest, graphql, data, account,
let pushed = 0 let pushed = 0
do { do {
console.debug(`metrics/compute/${login}/plugins > notable > retrieving user issues after ${cursor}`) console.debug(`metrics/compute/${login}/plugins > notable > retrieving user issues after ${cursor}`)
const {user:{issues:{edges}}} = await graphql(queries.notable.issues({login, type:"issues", after:cursor ? `after: "${cursor}"` : ""})) const {user: {issues: {edges}}} = await graphql(queries.notable.issues({login, type: "issues", after: cursor ? `after: "${cursor}"` : ""}))
cursor = edges?.[edges?.length - 1]?.cursor cursor = edges?.[edges?.length - 1]?.cursor
edges.map(({node:{repository:{nameWithOwner:repository}}}) => issues[repository] = (issues[repositories] ?? 0) + 1) edges.map(({node: {repository: {nameWithOwner: repository}}}) => issues[repository] = (issues[repositories] ?? 0) + 1)
pushed = edges.length pushed = edges.length
} while ((pushed) && (cursor)) } while ((pushed) && (cursor))
} }
@@ -60,9 +61,9 @@ export default async function({login, q, imports, rest, graphql, data, account,
let pushed = 0 let pushed = 0
do { do {
console.debug(`metrics/compute/${login}/plugins > notable > retrieving user pull requests after ${cursor}`) console.debug(`metrics/compute/${login}/plugins > notable > retrieving user pull requests after ${cursor}`)
const {user:{pullRequests:{edges}}} = await graphql(queries.notable.issues({login, type:"pullRequests", after:cursor ? `after: "${cursor}"` : ""})) const {user: {pullRequests: {edges}}} = await graphql(queries.notable.issues({login, type: "pullRequests", after: cursor ? `after: "${cursor}"` : ""}))
cursor = edges?.[edges?.length - 1]?.cursor cursor = edges?.[edges?.length - 1]?.cursor
edges.map(({node:{repository:{nameWithOwner:repository}}}) => pulls[repository] = (pulls[repositories] ?? 0) + 1) edges.map(({node: {repository: {nameWithOwner: repository}}}) => pulls[repository] = (pulls[repositories] ?? 0) + 1)
pushed = edges.length pushed = edges.length
} while ((pushed) && (cursor)) } while ((pushed) && (cursor))
} }
@@ -74,24 +75,24 @@ export default async function({login, q, imports, rest, graphql, data, account,
const [owner, repo] = handle.split("/") const [owner, repo] = handle.split("/")
try { try {
//Count total commits on repository //Count total commits on repository
const {repository:{defaultBranchRef:{target:{history}}}} = await graphql(queries.notable.commits({owner, repo})) const {repository: {defaultBranchRef: {target: {history}}}} = await graphql(queries.notable.commits({owner, repo}))
contribution.history = history.totalCount contribution.history = history.totalCount
//Load maintainers (errors probably means that token is not allowed to list contributors hence not a maintainer of said repo) //Load maintainers (errors probably means that token is not allowed to list contributors hence not a maintainer of said repo)
const {data:collaborators} = await rest.repos.listCollaborators({owner, repo}).catch(() => ({data:[]})) const {data: collaborators} = await rest.repos.listCollaborators({owner, repo}).catch(() => ({data: []}))
const maintainers = collaborators.filter(({role_name:role}) => ["admin", "maintain", "write"].includes(role)).map(({login}) => login) const maintainers = collaborators.filter(({role_name: role}) => ["admin", "maintain", "write"].includes(role)).map(({login}) => login)
//Count total commits of user //Count total commits of user
const {data:contributions = []} = await rest.repos.getContributorsStats({owner, repo}) const {data: contributions = []} = await rest.repos.getContributorsStats({owner, repo})
const commits = contributions.filter(({author}) => author.login.toLocaleLowerCase() === login.toLocaleLowerCase()).reduce((a, {total:b}) => a + b, 0) const commits = contributions.filter(({author}) => author.login.toLocaleLowerCase() === login.toLocaleLowerCase()).reduce((a, {total: b}) => a + b, 0)
//Save user data //Save user data
contribution.user = { contribution.user = {
commits, commits,
percentage:commits / contribution.history, percentage: commits / contribution.history,
maintainer:maintainers.includes(login), maintainer: maintainers.includes(login),
issues:issues[handle] ?? 0, issues: issues[handle] ?? 0,
pulls:pulls[handle] ?? 0, pulls: pulls[handle] ?? 0,
get stars() { get stars() {
return Math.round(this.maintainer ? stars : this.percentage * stars) return Math.round(this.maintainer ? stars : this.percentage * stars)
}, },
@@ -115,7 +116,7 @@ export default async function({login, q, imports, rest, graphql, data, account,
const aggregate = aggregated.get(key) const aggregate = aggregated.get(key)
aggregate.aggregated++ aggregate.aggregated++
if (indepth) { if (indepth) {
const {history = 0, user:{commits = 0, percentage = 0, maintainer = false} = {}} = _extras const {history = 0, user: {commits = 0, percentage = 0, maintainer = false} = {}} = _extras
aggregate.history = aggregate.history ?? 0 aggregate.history = aggregate.history ?? 0
aggregate.history += history aggregate.history += history
aggregate.user = aggregate.user ?? {} aggregate.user = aggregate.user ?? {}
@@ -124,16 +125,16 @@ export default async function({login, q, imports, rest, graphql, data, account,
aggregate.user.maintainer = aggregate.user.maintainer || maintainer aggregate.user.maintainer = aggregate.user.maintainer || maintainer
} }
} }
else else {
aggregated.set(key, {name:key, handle, avatar, organization, stars, aggregated:1, ..._extras}) aggregated.set(key, {name: key, handle, avatar, organization, stars, aggregated: 1, ..._extras})
}
} }
contributions = [...aggregated.values()] contributions = [...aggregated.values()]
if (indepth) { if (indepth) {
//Normalize contribution percentage //Normalize contribution percentage
contributions.map(aggregate => aggregate.user ? aggregate.user.percentage /= aggregate.aggregated : null) contributions.map(aggregate => aggregate.user ? aggregate.user.percentage /= aggregate.aggregated : null)
//Additional filtering (no user commits means that API wasn't able to answer back, considering it as matching by default) //Additional filtering (no user commits means that API wasn't able to answer back, considering it as matching by default)
contributions = contributions.filter(({handle, user}) => !user?.commits ? true : imports.ghfilter(filter, {handle, commits:contributions.history, "commits.user":user.commits, "commits.user%":user.percentage * 100, maintainer:user.maintainer})) contributions = contributions.filter(({handle, user}) => !user?.commits ? true : imports.ghfilter(filter, {handle, commits: contributions.history, "commits.user": user.commits, "commits.user%": user.percentage * 100, maintainer: user.maintainer}))
//Sort contribution by maintainer first and then by contribution percentage //Sort contribution by maintainer first and then by contribution percentage
contributions = contributions.sort((a, b) => ((b.user?.percentage + b.user?.maintainer) || 0) - ((a.user?.percentage + a.user?.maintainer) || 0)) contributions = contributions.sort((a, b) => ((b.user?.percentage + b.user?.maintainer) || 0) - ((a.user?.percentage + a.user?.maintainer) || 0))
} }
@@ -143,6 +144,6 @@ export default async function({login, q, imports, rest, graphql, data, account,
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -12,7 +12,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
if (!/^https?:[/][/]/.test(url)) if (!/^https?:[/][/]/.test(url))
url = `https://${url}` url = `https://${url}`
const {protocol, host} = imports.url.parse(url) const {protocol, host} = imports.url.parse(url)
const result = {url:`${protocol}//${host}`, detailed, scores:[], metrics:{}} const result = {url: `${protocol}//${host}`, detailed, scores: [], metrics: {}}
//Load scores from API //Load scores from API
console.debug(`metrics/compute/${login}/plugins > pagespeed > querying api for ${result.url}`) console.debug(`metrics/compute/${login}/plugins > pagespeed > querying api for ${result.url}`)
const scores = new Map() const scores = new Map()
@@ -53,6 +53,6 @@ export default async function({login, imports, data, q, account}, {enabled = fal
message = `API returned ${status}${description ? ` (${description})` : ""}` message = `API returned ${status}${description ? ` (${description})` : ""}`
error = error.response?.data ?? null error = error.response?.data ?? null
} }
throw {error:{message, instance:error}} throw {error: {message, instance: error}}
} }
} }

View File

@@ -8,20 +8,20 @@ export default async function({login, data, graphql, rest, q, queries, imports,
//Context //Context
let context = { let context = {
mode:"user", mode: "user",
types:account === "organization" ? ["sponsorshipsAsMaintainer", "sponsorshipsAsSponsor", "membersWithRole", "thanks"] : ["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor", "thanks"], types: account === "organization" ? ["sponsorshipsAsMaintainer", "sponsorshipsAsSponsor", "membersWithRole", "thanks"] : ["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor", "thanks"],
default:"followers, following", default: "followers, following",
alias:{followed:"following", sponsors:"sponsorshipsAsMaintainer", sponsored:"sponsorshipsAsSponsor", sponsoring:"sponsorshipsAsSponsor", members:"membersWithRole"}, alias: {followed: "following", sponsors: "sponsorshipsAsMaintainer", sponsored: "sponsorshipsAsSponsor", sponsoring: "sponsorshipsAsSponsor", members: "membersWithRole"},
sponsorships:{sponsorshipsAsMaintainer:"sponsorEntity", sponsorshipsAsSponsor:"sponsorable"}, sponsorships: {sponsorshipsAsMaintainer: "sponsorEntity", sponsorshipsAsSponsor: "sponsorable"},
} }
if (q.repo) { if (q.repo) {
console.debug(`metrics/compute/${login}/plugins > people > switched to repository mode`) console.debug(`metrics/compute/${login}/plugins > people > switched to repository mode`)
const {owner, repo} = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})).shift() const {owner, repo} = data.user.repositories.nodes.map(({name: repo, owner: {login: owner}}) => ({repo, owner})).shift()
context = {...context, mode:"repository", types:["contributors", "stargazers", "watchers", "sponsorshipsAsMaintainer", "thanks"], default:"stargazers, watchers", owner, repo} context = {...context, mode: "repository", types: ["contributors", "stargazers", "watchers", "sponsorshipsAsMaintainer", "thanks"], default: "stargazers, watchers", owner, repo}
} }
//Load inputs //Load inputs
let {limit, types, size, identicons, "identicons.hide":_identicons_hide, thanks, shuffle, "sponsors.custom":_sponsors} = imports.metadata.plugins.people.inputs({data, account, q}, {types:context.default}) let {limit, types, size, identicons, "identicons.hide": _identicons_hide, thanks, shuffle, "sponsors.custom": _sponsors} = imports.metadata.plugins.people.inputs({data, account, q}, {types: context.default})
//Filter types //Filter types
types = [...new Set([...types].map(type => (context.alias[type] ?? type)).filter(type => context.types.includes(type)) ?? [])] types = [...new Set([...types].map(type => (context.alias[type] ?? type)).filter(type => context.types.includes(type)) ?? [])]
if ((types.includes("sponsorshipsAsMaintainer")) && (_sponsors?.length)) { if ((types.includes("sponsorshipsAsMaintainer")) && (_sponsors?.length)) {
@@ -38,13 +38,13 @@ export default async function({login, data, graphql, rest, q, queries, imports,
//Rest //Rest
if (type === "contributors") { if (type === "contributors") {
const {owner, repo} = context const {owner, repo} = context
const {data:nodes} = await rest.repos.listContributors({owner, repo}) const {data: nodes} = await rest.repos.listContributors({owner, repo})
result[type].push(...nodes.map(({login, avatar_url}) => ({login, avatarUrl:avatar_url}))) result[type].push(...nodes.map(({login, avatar_url}) => ({login, avatarUrl: avatar_url})))
} }
else if ((type === "thanks") || (type === "sponsorshipsCustom")) { else if ((type === "thanks") || (type === "sponsorshipsCustom")) {
const users = {thanks, sponsorshipsCustom:_sponsors}[type] ?? [] const users = {thanks, sponsorshipsCustom: _sponsors}[type] ?? []
const nodes = await Promise.all(users.map(async username => (await rest.users.getByUsername({username})).data)) const nodes = await Promise.all(users.map(async username => (await rest.users.getByUsername({username})).data))
result[{sponsorshipsCustom:"sponsorshipsAsMaintainer"}[type] ?? type].push(...nodes.map(({login, avatar_url}) => ({login, avatarUrl:avatar_url}))) result[{sponsorshipsCustom: "sponsorshipsAsMaintainer"}[type] ?? type].push(...nodes.map(({login, avatar_url}) => ({login, avatarUrl: avatar_url})))
} }
//GraphQL //GraphQL
else { else {
@@ -52,12 +52,12 @@ export default async function({login, data, graphql, rest, q, queries, imports,
let pushed = 0 let pushed = 0
do { do {
console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type} after ${cursor}`) console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type} after ${cursor}`)
const {[type]:{edges}} = ( const {[type]: {edges}} = (
type in context.sponsorships type in context.sponsorships
? (await graphql(queries.people.sponsors({login:context.owner ?? login, type, size, after:cursor ? `after: "${cursor}"` : "", target:context.sponsorships[type], account})))[account] ? (await graphql(queries.people.sponsors({login: context.owner ?? login, type, size, after: cursor ? `after: "${cursor}"` : "", target: context.sponsorships[type], account})))[account]
: context.mode === "repository" : context.mode === "repository"
? (await graphql(queries.people.repository({login:context.owner, repository:context.repo, type, size, after:cursor ? `after: "${cursor}"` : "", account})))[account].repository ? (await graphql(queries.people.repository({login: context.owner, repository: context.repo, type, size, after: cursor ? `after: "${cursor}"` : "", account})))[account].repository
: (await graphql(queries.people({login, type, size, after:cursor ? `after: "${cursor}"` : "", account})))[account] : (await graphql(queries.people({login, type, size, after: cursor ? `after: "${cursor}"` : "", account})))[account]
) )
cursor = edges?.[edges?.length - 1]?.cursor cursor = edges?.[edges?.length - 1]?.cursor
result[type].push(...edges.map(({node}) => node[context.sponsorships[type]] ?? node)) result[type].push(...edges.map(({node}) => node[context.sponsorships[type]] ?? node))
@@ -102,6 +102,6 @@ export default async function({login, data, graphql, rest, q, queries, imports,
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -17,21 +17,21 @@ export default async function({login, data, imports, q, queries, account}, {enab
//Dev.to //Dev.to
case "dev.to": { case "dev.to": {
console.debug(`metrics/compute/${login}/plugins > posts > querying api`) console.debug(`metrics/compute/${login}/plugins > posts > querying api`)
posts = (await imports.axios.get(`https://dev.to/api/articles?username=${user}&state=fresh`)).data.map(({title, description, published_at:date, cover_image:image, url:link}) => ({title, description, date, image, link})) posts = (await imports.axios.get(`https://dev.to/api/articles?username=${user}&state=fresh`)).data.map(({title, description, published_at: date, cover_image: image, url: link}) => ({title, description, date, image, link}))
link = `https://dev.to/${user}` link = `https://dev.to/${user}`
break break
} }
//Hashnode //Hashnode
case "hashnode": { case "hashnode": {
posts = (await imports.axios.post("https://api.hashnode.com", {query:queries.posts.hashnode({user})}, {headers:{"Content-type":"application/json"}})).data.data.user.publication.posts.map(( posts = (await imports.axios.post("https://api.hashnode.com", {query: queries.posts.hashnode({user})}, {headers: {"Content-type": "application/json"}})).data.data.user.publication.posts.map((
{title, brief:description, dateAdded:date, coverImage:image, slug}, {title, brief: description, dateAdded: date, coverImage: image, slug},
) => ({title, description, date, image, link:`https://hashnode.com/post/${slug}`})) ) => ({title, description, date, image, link: `https://hashnode.com/post/${slug}`}))
link = `https://hashnode.com/@${user}` link = `https://hashnode.com/@${user}`
break break
} }
//Unsupported //Unsupported
default: default:
throw {error:{message:`Unsupported source "${source}"`}} throw {error: {message: `Unsupported source "${source}"`}}
} }
//Format posts //Format posts
@@ -44,19 +44,19 @@ export default async function({login, data, imports, q, queries, account}, {enab
//Cover images //Cover images
if (covers) { if (covers) {
console.debug(`metrics/compute/${login}/plugins > posts > formatting cover images`) console.debug(`metrics/compute/${login}/plugins > posts > formatting cover images`)
posts = await Promise.all(posts.map(async ({image, ...post}) => ({image:await imports.imgb64(image, {width:144, height:-1}), ...post}))) posts = await Promise.all(posts.map(async ({image, ...post}) => ({image: await imports.imgb64(image, {width: 144, height: -1}), ...post})))
} }
//Results //Results
return {user, source, link, descriptions, covers, list:posts} return {user, source, link, descriptions, covers, list: posts}
} }
//Unhandled error //Unhandled error
throw {error:{message:"An error occured (could not retrieve posts)"}} throw {error: {message: "An error occured (could not retrieve posts)"}}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -15,7 +15,7 @@ export default async function({login, data, imports, graphql, q, queries, accoun
//Retrieve user owned projects from graphql api //Retrieve user owned projects from graphql api
console.debug(`metrics/compute/${login}/plugins > projects > querying api`) console.debug(`metrics/compute/${login}/plugins > projects > querying api`)
const {[account]:{projects}} = await graphql(queries.projects.user({login, limit, account})) const {[account]: {projects}} = await graphql(queries.projects.user({login, limit, account}))
//Retrieve repositories projects from graphql api //Retrieve repositories projects from graphql api
for (const identifier of repositories) { for (const identifier of repositories) {
@@ -25,7 +25,7 @@ export default async function({login, data, imports, graphql, q, queries, accoun
let project = null let project = null
for (const account of ["user", "organization"]) { for (const account of ["user", "organization"]) {
try { try {
({project} = (await graphql(queries.projects.repository({user, repository, id, account})))[account].repository) ;({project} = (await graphql(queries.projects.repository({user, repository, id, account})))[account].repository)
} }
catch (error) { catch (error) {
console.debug(error) console.debug(error)
@@ -52,9 +52,9 @@ export default async function({login, data, imports, graphql, q, queries, accoun
else if (time < 30) else if (time < 30)
updated = `${Math.floor(time)} day${time >= 2 ? "s" : ""} ago` updated = `${Math.floor(time)} day${time >= 2 ? "s" : ""} ago`
//Format progress //Format progress
const {enabled, todoCount:todo, inProgressCount:doing, doneCount:done} = project.progress const {enabled, todoCount: todo, inProgressCount: doing, doneCount: done} = project.progress
//Append //Append
list.push({name:project.name, updated, description:project.body, progress:{enabled, todo, doing, done, total:todo + doing + done}}) list.push({name: project.name, updated, description: project.body, progress: {enabled, todo, doing, done, total: todo + doing + done}})
} }
//Limit //Limit
@@ -62,13 +62,13 @@ export default async function({login, data, imports, graphql, q, queries, accoun
list.splice(limit) list.splice(limit)
//Results //Results
return {list, totalCount:projects.totalCount, descriptions} return {list, totalCount: projects.totalCount, descriptions}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
let message = "An error occured" let message = "An error occured"
if (error.errors?.map(({type}) => type)?.includes("INSUFFICIENT_SCOPES")) if (error.errors?.map(({type}) => type)?.includes("INSUFFICIENT_SCOPES"))
message = "Insufficient token rights" message = "Insufficient token rights"
throw {error:{message, instance:error}} throw {error: {message, instance: error}}
} }
} }

View File

@@ -7,23 +7,23 @@ export default async function({login, q, imports, data, graphql, queries, accoun
return null return null
//Load inputs //Load inputs
let {limit:_limit1, "limit.issues":_limit2, "limit.discussions":_limit3, "limit.discussions.comments":_limit4, days, details, display, ignored} = imports.metadata.plugins.reactions.inputs({data, account, q}) let {limit: _limit1, "limit.issues": _limit2, "limit.discussions": _limit3, "limit.discussions.comments": _limit4, days, details, display, ignored} = imports.metadata.plugins.reactions.inputs({data, account, q})
ignored.push(...data.shared["users.ignored"]) ignored.push(...data.shared["users.ignored"])
//Load issue comments //Load issue comments
const comments = [] const comments = []
for (const {type, limit} of [{type:"issueComments", limit:_limit1}, {type:"issues", limit:_limit2}, {type:"repositoryDiscussionComments", limit:_limit3}, {type:"repositoryDiscussions", limit:_limit4}].filter(({limit}) => limit)) { for (const {type, limit} of [{type: "issueComments", limit: _limit1}, {type: "issues", limit: _limit2}, {type: "repositoryDiscussionComments", limit: _limit3}, {type: "repositoryDiscussions", limit: _limit4}].filter(({limit}) => limit)) {
let cursor = null, pushed = 0 let cursor = null, pushed = 0
const fetched = [] const fetched = []
try { try {
do { do {
//Load issue comments //Load issue comments
console.debug(`metrics/compute/${login}/plugins > reactions > retrieving ${type} before ${cursor}`) console.debug(`metrics/compute/${login}/plugins > reactions > retrieving ${type} before ${cursor}`)
const {user:{[type]:{edges}}} = await graphql(queries.reactions({login, type, before:cursor ? `before: "${cursor}"` : ""})) const {user: {[type]: {edges}}} = await graphql(queries.reactions({login, type, before: cursor ? `before: "${cursor}"` : ""}))
cursor = edges?.[0]?.cursor cursor = edges?.[0]?.cursor
//Save issue comments //Save issue comments
const filtered = edges const filtered = edges
.flatMap(({node:{createdAt:created, reactions:{nodes:reactions}}}) => ({created:new Date(created), reactions:reactions.filter(({user = {}}) => !ignored.includes(user.login)).map(({content}) => content)})) .flatMap(({node: {createdAt: created, reactions: {nodes: reactions}}}) => ({created: new Date(created), reactions: reactions.filter(({user = {}}) => !ignored.includes(user.login)).map(({content}) => content)}))
.filter(comment => Number.isFinite(days) ? comment.created < new Date(Date.now() - days * 24 * 60 * 60 * 1000) : true) .filter(comment => Number.isFinite(days) ? comment.created < new Date(Date.now() - days * 24 * 60 * 60 * 1000) : true)
pushed = filtered.length pushed = filtered.length
fetched.push(...filtered) fetched.push(...filtered)
@@ -50,13 +50,13 @@ export default async function({login, q, imports, data, graphql, queries, accoun
list[reaction] = (list[reaction] ?? 0) + 1 list[reaction] = (list[reaction] ?? 0) + 1
const max = Math.max(...Object.values(list)) const max = Math.max(...Object.values(list))
for (const [key, value] of Object.entries(list)) for (const [key, value] of Object.entries(list))
list[key] = {value, percentage:value / reactions.length, score:value / (display === "relative" ? max : reactions.length)} list[key] = {value, percentage: value / reactions.length, score: value / (display === "relative" ? max : reactions.length)}
//Results //Results
return {list, comments:comments.length, details, days, twemoji:q["config.twemoji"]} return {list, comments: comments.length, details, days, twemoji: q["config.twemoji"]}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -10,7 +10,7 @@ export default async function({login, q, imports, graphql, queries, data, accoun
let {featured} = imports.metadata.plugins.repositories.inputs({data, account, q}) let {featured} = imports.metadata.plugins.repositories.inputs({data, account, q})
//Initialization //Initialization
const repositories = {list:[]} const repositories = {list: []}
//Fetch repositories informations //Fetch repositories informations
for (const repo of featured) { for (const repo of featured) {
@@ -33,6 +33,6 @@ export default async function({login, q, imports, graphql, queries, data, accoun
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -9,11 +9,11 @@ export default async function({login, q, imports, data, account}, {enabled = fal
//Load inputs //Load inputs
let {source, limit} = imports.metadata.plugins.rss.inputs({data, account, q}) let {source, limit} = imports.metadata.plugins.rss.inputs({data, account, q})
if (!source) if (!source)
throw {error:{message:"A RSS feed is required"}} throw {error: {message: "A RSS feed is required"}}
//Load rss feed //Load rss feed
const {title, description, link, items} = await (new imports.rss()).parseURL(source) //eslint-disable-line new-cap const {title, description, link, items} = await (new imports.rss()).parseURL(source) //eslint-disable-line new-cap
const feed = items.map(({title, link, isoDate:date}) => ({title, link, date:new Date(date)})) const feed = items.map(({title, link, isoDate: date}) => ({title, link, date: new Date(date)}))
//Limit feed //Limit feed
if (limit > 0) { if (limit > 0) {
@@ -22,12 +22,12 @@ export default async function({login, q, imports, data, account}, {enabled = fal
} }
//Results //Results
return {source:title, description, link, feed} return {source: title, description, link, feed}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -24,15 +24,15 @@ export default async function({login, q, imports, data, account}, {enabled = fal
//Load page //Load page
console.debug(`metrics/compute/${login}/plugins > skyline > loading skyline.github.com/${login}/${year}`) console.debug(`metrics/compute/${login}/plugins > skyline > loading skyline.github.com/${login}/${year}`)
await page.goto(`https://skyline.github.com/${login}/${year}`, {timeout:90 * 1000}) await page.goto(`https://skyline.github.com/${login}/${year}`, {timeout: 90 * 1000})
console.debug(`metrics/compute/${login}/plugins > skyline > waiting for initial render`) console.debug(`metrics/compute/${login}/plugins > skyline > waiting for initial render`)
const frame = page.mainFrame() const frame = page.mainFrame()
await page.waitForFunction('[...document.querySelectorAll("span")].map(span => span.innerText).includes("Share on Twitter")', {timeout:90 * 1000}) await page.waitForFunction('[...document.querySelectorAll("span")].map(span => span.innerText).includes("Share on Twitter")', {timeout: 90 * 1000})
await frame.evaluate(() => [...document.querySelectorAll("button, footer, a")].map(element => element.remove())) await frame.evaluate(() => [...document.querySelectorAll("button, footer, a")].map(element => element.remove()))
//Generate gif //Generate gif
console.debug(`metrics/compute/${login}/plugins > skyline > generating frames`) console.debug(`metrics/compute/${login}/plugins > skyline > generating frames`)
const animation = compatibility ? await imports.record({page, width, height, frames, scale:quality}) : await imports.gif({page, width, height, frames, quality:Math.max(1, quality * 20)}) const animation = compatibility ? await imports.record({page, width, height, frames, scale: quality}) : await imports.gif({page, width, height, frames, quality: Math.max(1, quality * 20)})
//Close puppeteer //Close puppeteer
await browser.close() await browser.close()
@@ -42,6 +42,6 @@ export default async function({login, q, imports, data, account}, {enabled = fal
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -11,10 +11,10 @@ export default async function({login, q, imports, data, graphql, queries, accoun
//Query description and goal //Query description and goal
console.debug(`metrics/compute/${login}/plugins > sponsors > querying sponsors and goal`) console.debug(`metrics/compute/${login}/plugins > sponsors > querying sponsors and goal`)
const {[account]:{sponsorsListing:{fullDescription, activeGoal}}} = await graphql(queries.sponsors.description({login, account})) const {[account]: {sponsorsListing: {fullDescription, activeGoal}}} = await graphql(queries.sponsors.description({login, account}))
const about = await imports.markdown(fullDescription, {mode:"multiline"}) const about = await imports.markdown(fullDescription, {mode: "multiline"})
const goal = activeGoal ? {progress:activeGoal.percentComplete, title:activeGoal.title, description:await imports.markdown(activeGoal.description)} : null const goal = activeGoal ? {progress: activeGoal.percentComplete, title: activeGoal.title, description: await imports.markdown(activeGoal.description)} : null
const count = {total:{count:0, user:0, organization:0}, active:{total:0, user:0, organization:0}, past:{total:0, user:0, organization:0}} const count = {total: {count: 0, user: 0, organization: 0}, active: {total: 0, user: 0, organization: 0}, past: {total: 0, user: 0, organization: 0}}
//Query active sponsors //Query active sponsors
let list = [] let list = []
@@ -24,13 +24,13 @@ export default async function({login, q, imports, data, graphql, queries, accoun
let pushed = 0 let pushed = 0
do { do {
console.debug(`metrics/compute/${login}/sponsors > retrieving sponsors after ${cursor}`) console.debug(`metrics/compute/${login}/sponsors > retrieving sponsors after ${cursor}`)
const {[account]:{sponsorshipsAsMaintainer:{edges, nodes}}} = await graphql(queries.sponsors.active({login, account, after:cursor ? `after: "${cursor}"` : "", size:Math.round(size*1.5)})) const {[account]: {sponsorshipsAsMaintainer: {edges, nodes}}} = await graphql(queries.sponsors.active({login, account, after: cursor ? `after: "${cursor}"` : "", size: Math.round(size * 1.5)}))
cursor = edges?.[edges?.length - 1]?.cursor cursor = edges?.[edges?.length - 1]?.cursor
fetched.push(...nodes) fetched.push(...nodes)
pushed = nodes.length pushed = nodes.length
console.debug(`metrics/compute/${login}/sponsors > retrieved ${pushed} sponsors after ${cursor}`) console.debug(`metrics/compute/${login}/sponsors > retrieved ${pushed} sponsors after ${cursor}`)
} while ((pushed) && (cursor)) } while ((pushed) && (cursor))
list.push(...fetched.map(({sponsorEntity:{login, avatarUrl, url:organization = null}, tier}) => ({login, avatarUrl, type:organization ? "organization" : "user", amount:tier?.monthlyPriceInDollars ?? null, past:false}))) list.push(...fetched.map(({sponsorEntity: {login, avatarUrl, url: organization = null}, tier}) => ({login, avatarUrl, type: organization ? "organization" : "user", amount: tier?.monthlyPriceInDollars ?? null, past: false})))
await Promise.all(list.map(async user => user.avatar = await imports.imgb64(user.avatarUrl))) await Promise.all(list.map(async user => user.avatar = await imports.imgb64(user.avatarUrl)))
count.active.total = list.length count.active.total = list.length
count.active.user = list.filter(user => user.type === "user").length count.active.user = list.filter(user => user.type === "user").length
@@ -48,18 +48,18 @@ export default async function({login, q, imports, data, graphql, queries, accoun
let pushed = 0 let pushed = 0
do { do {
console.debug(`metrics/compute/${login}/sponsors > retrieving sponsors events after ${cursor}`) console.debug(`metrics/compute/${login}/sponsors > retrieving sponsors events after ${cursor}`)
const {[account]:{sponsorsActivities:{edges, nodes}}} = await graphql(queries.sponsors.all({login, account, after:cursor ? `after: "${cursor}"` : "", size:Math.round(size*1.5)})) const {[account]: {sponsorsActivities: {edges, nodes}}} = await graphql(queries.sponsors.all({login, account, after: cursor ? `after: "${cursor}"` : "", size: Math.round(size * 1.5)}))
cursor = edges?.[edges?.length - 1]?.cursor cursor = edges?.[edges?.length - 1]?.cursor
fetched.push(...nodes) fetched.push(...nodes)
pushed = nodes.length pushed = nodes.length
console.debug(`metrics/compute/${login}/sponsors > retrieved ${pushed} sponsors events after ${cursor}`) console.debug(`metrics/compute/${login}/sponsors > retrieved ${pushed} sponsors events after ${cursor}`)
} while ((pushed) && (cursor)) } while ((pushed) && (cursor))
users.push(...fetched.map(({sponsor:{login, avatarUrl, url:organization = null}, sponsorsTier}) => ({login, avatarUrl, type:organization ? "organization" : "user", amount:sponsorsTier?.monthlyPriceInDollars ?? null, past:true}))) users.push(...fetched.map(({sponsor: {login, avatarUrl, url: organization = null}, sponsorsTier}) => ({login, avatarUrl, type: organization ? "organization" : "user", amount: sponsorsTier?.monthlyPriceInDollars ?? null, past: true})))
} }
for (const user of users) { for (const user of users) {
if (!active.has(user.login)) { if (!active.has(user.login)) {
active.add(user.login) active.add(user.login)
list.push({...user, avatar:await imports.imgb64(user.avatarUrl)}) list.push({...user, avatar: await imports.imgb64(user.avatarUrl)})
count.past.total++ count.past.total++
count.past[user.type]++ count.past[user.type]++
} }
@@ -77,6 +77,6 @@ export default async function({login, q, imports, data, graphql, queries, accoun
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -7,54 +7,54 @@ export default async function({login, q, imports, data, account}, {enabled = fal
return null return null
//Load inputs //Load inputs
let {sections, user, limit, lines, "lines.snippet":codelines} = imports.metadata.plugins.stackoverflow.inputs({data, account, q}) let {sections, user, limit, lines, "lines.snippet": codelines} = imports.metadata.plugins.stackoverflow.inputs({data, account, q})
if (!user) if (!user)
throw {error:{message:"You must provide a stackoverflow user id"}} throw {error: {message: "You must provide a stackoverflow user id"}}
//Initialization //Initialization
//See https://api.stackexchange.com/docs //See https://api.stackexchange.com/docs
const api = {base:"https://api.stackexchange.com/2.2", user:`https://api.stackexchange.com/2.2/users/${user}`} const api = {base: "https://api.stackexchange.com/2.2", user: `https://api.stackexchange.com/2.2/users/${user}`}
const filters = {user:"!0Z-LvgkLYnTCu1858)*D0lcx2", answer:"!7goY5TLWwCz.BaGpe)tv5C6Bks2q8siMH6", question:"!)EhwvzgX*hrClxjLzqxiZHHbTPRE5Pb3B9vvRaqCx5-ZY.vPr"} const filters = {user: "!0Z-LvgkLYnTCu1858)*D0lcx2", answer: "!7goY5TLWwCz.BaGpe)tv5C6Bks2q8siMH6", question: "!)EhwvzgX*hrClxjLzqxiZHHbTPRE5Pb3B9vvRaqCx5-ZY.vPr"}
const result = {sections, lines} const result = {sections, lines}
//Stackoverflow user metrics //Stackoverflow user metrics
{ {
//Account metrics //Account metrics
console.debug(`metrics/compute/${login}/plugins > stackoverflow > querying api for user ${user}`) console.debug(`metrics/compute/${login}/plugins > stackoverflow > querying api for user ${user}`)
const {data:{items:[{reputation, badge_counts:{bronze, silver, gold}, answer_count:answers, question_count:questions, view_count:views}]}} = await imports.axios.get(`${api.user}?site=stackoverflow&filter=${filters.user}`) const {data: {items: [{reputation, badge_counts: {bronze, silver, gold}, answer_count: answers, question_count: questions, view_count: views}]}} = await imports.axios.get(`${api.user}?site=stackoverflow&filter=${filters.user}`)
const {data:{total:comments}} = await imports.axios.get(`${api.user}/comments?site=stackoverflow&filter=total`) const {data: {total: comments}} = await imports.axios.get(`${api.user}/comments?site=stackoverflow&filter=total`)
//Save result //Save result
result.user = {id:user, reputation, badges:bronze + silver + gold, questions, answers, comments, views} result.user = {id: user, reputation, badges: bronze + silver + gold, questions, answers, comments, views}
} }
//Answers //Answers
for (const {key, sort} of [{key:"answers-recent", sort:"sort=activity&order=desc"}, {key:"answers-top", sort:"sort=votes&order=desc"}].filter(({key}) => sections.includes(key))) { for (const {key, sort} of [{key: "answers-recent", sort: "sort=activity&order=desc"}, {key: "answers-top", sort: "sort=votes&order=desc"}].filter(({key}) => sections.includes(key))) {
//Load and format answers //Load and format answers
console.debug(`metrics/compute/${login}/plugins > stackoverflow > querying api for ${key}`) console.debug(`metrics/compute/${login}/plugins > stackoverflow > querying api for ${key}`)
const {data:{items}} = await imports.axios.get(`${api.user}/answers?site=stackoverflow&pagesize=${limit}&filter=${filters.answer}&${sort}`) const {data: {items}} = await imports.axios.get(`${api.user}/answers?site=stackoverflow&pagesize=${limit}&filter=${filters.answer}&${sort}`)
result[key] = await Promise.all(items.map(item => format.answer(item, {imports, codelines}))) result[key] = await Promise.all(items.map(item => format.answer(item, {imports, codelines})))
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loaded ${result[key].length} items`) console.debug(`metrics/compute/${login}/plugins > stackoverflow > loaded ${result[key].length} items`)
//Load related questions //Load related questions
const ids = result[key].map(({question_id}) => question_id).filter(id => id) const ids = result[key].map(({question_id}) => question_id).filter(id => id)
if (ids) { if (ids) {
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loading ${ids.length} related items`) console.debug(`metrics/compute/${login}/plugins > stackoverflow > loading ${ids.length} related items`)
const {data:{items}} = await imports.axios.get(`${api.base}/questions/${ids.join(";")}?site=stackoverflow&filter=${filters.question}`) const {data: {items}} = await imports.axios.get(`${api.base}/questions/${ids.join(";")}?site=stackoverflow&filter=${filters.question}`)
await Promise.all(items.map(item => format.question(item, {imports, codelines}))) await Promise.all(items.map(item => format.question(item, {imports, codelines})))
} }
} }
//Questions //Questions
for (const {key, sort} of [{key:"questions-recent", sort:"sort=activity&order=desc"}, {key:"questions-top", sort:"sort=votes&order=desc"}].filter(({key}) => sections.includes(key))) { for (const {key, sort} of [{key: "questions-recent", sort: "sort=activity&order=desc"}, {key: "questions-top", sort: "sort=votes&order=desc"}].filter(({key}) => sections.includes(key))) {
//Load and format questions //Load and format questions
console.debug(`metrics/compute/${login}/plugins > stackoverflow > querying api for ${key}`) console.debug(`metrics/compute/${login}/plugins > stackoverflow > querying api for ${key}`)
const {data:{items}} = await imports.axios.get(`${api.user}/questions?site=stackoverflow&pagesize=${limit}&filter=${filters.question}&${sort}`) const {data: {items}} = await imports.axios.get(`${api.user}/questions?site=stackoverflow&pagesize=${limit}&filter=${filters.question}&${sort}`)
result[key] = await Promise.all(items.map(item => format.question(item, {imports, codelines}))) result[key] = await Promise.all(items.map(item => format.question(item, {imports, codelines})))
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loaded ${result[key].length} items`) console.debug(`metrics/compute/${login}/plugins > stackoverflow > loaded ${result[key].length} items`)
//Load related answers //Load related answers
const ids = result[key].map(({accepted_answer_id}) => accepted_answer_id).filter(id => id) const ids = result[key].map(({accepted_answer_id}) => accepted_answer_id).filter(id => id)
if (ids) { if (ids) {
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loading ${ids.length} related items`) console.debug(`metrics/compute/${login}/plugins > stackoverflow > loading ${ids.length} related items`)
const {data:{items}} = await imports.axios.get(`${api.base}/answers/${ids.join(";")}?site=stackoverflow&filter=${filters.answer}`) const {data: {items}} = await imports.axios.get(`${api.base}/answers/${ids.join(";")}?site=stackoverflow&filter=${filters.answer}`)
await Promise.all(items.map(item => format.answer(item, {imports, codelines}))) await Promise.all(items.map(item => format.answer(item, {imports, codelines})))
} }
} }
@@ -66,30 +66,30 @@ export default async function({login, q, imports, data, account}, {enabled = fal
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }
//Formatters //Formatters
const format = { const format = {
/**Cached */ /**Cached */
cached:new Map(), cached: new Map(),
/**Format stackoverflow code snippets */ /**Format stackoverflow code snippets */
code(text) { code(text) {
return text.replace(/<!-- language: lang-(?<lang>\w+) -->\s*(?<snippet> {4}[\s\S]+?)(?=(?:<!-- end snippet -->)|(?:<!-- language: lang-))/g, "```$<lang>\n$<snippet>```") return text.replace(/<!-- language: lang-(?<lang>\w+) -->\s*(?<snippet> {4}[\s\S]+?)(?=(?:<!-- end snippet -->)|(?:<!-- language: lang-))/g, "```$<lang>\n$<snippet>```")
}, },
/**Format answers */ /**Format answers */
async answer({body_markdown:body, score, up_vote_count:upvotes, down_vote_count:downvotes, is_accepted:accepted, comment_count:comments = 0, creation_date, owner:{display_name:author}, link, answer_id:id, question_id}, {imports, codelines}) { async answer({body_markdown: body, score, up_vote_count: upvotes, down_vote_count: downvotes, is_accepted: accepted, comment_count: comments = 0, creation_date, owner: {display_name: author}, link, answer_id: id, question_id}, {imports, codelines}) {
const formatted = { const formatted = {
type:"answer", type: "answer",
body:await imports.markdown(format.code(imports.htmlunescape(body)), {codelines}), body: await imports.markdown(format.code(imports.htmlunescape(body)), {codelines}),
score, score,
upvotes, upvotes,
downvotes, downvotes,
accepted, accepted,
comments, comments,
author, author,
created:imports.format.date(creation_date * 1000, {date:true}), created: imports.format.date(creation_date * 1000, {date: true}),
link, link,
id, id,
question_id, question_id,
@@ -104,28 +104,28 @@ const format = {
async question( async question(
{ {
title, title,
body_markdown:body, body_markdown: body,
score, score,
up_vote_count:upvotes, up_vote_count: upvotes,
down_vote_count:downvotes, down_vote_count: downvotes,
favorite_count:favorites, favorite_count: favorites,
tags, tags,
is_answered:answered, is_answered: answered,
answer_count:answers, answer_count: answers,
comment_count:comments = 0, comment_count: comments = 0,
view_count:views, view_count: views,
creation_date, creation_date,
owner:{display_name:author}, owner: {display_name: author},
link, link,
question_id:id, question_id: id,
accepted_answer_id = null, accepted_answer_id = null,
}, },
{imports, codelines}, {imports, codelines},
) { ) {
const formatted = { const formatted = {
type:"question", type: "question",
title:await imports.markdown(title), title: await imports.markdown(title),
body:await imports.markdown(format.code(imports.htmlunescape(body)), {codelines}), body: await imports.markdown(format.code(imports.htmlunescape(body)), {codelines}),
score, score,
upvotes, upvotes,
downvotes, downvotes,
@@ -136,7 +136,7 @@ const format = {
comments, comments,
views, views,
author, author,
created:imports.format.date(creation_date * 1000, {date:true}), created: imports.format.date(creation_date * 1000, {date: true}),
link, link,
id, id,
accepted_answer_id, accepted_answer_id,

View File

@@ -7,11 +7,11 @@ export default async function({login, graphql, data, imports, q, queries, accoun
return null return null
//Load inputs //Load inputs
let {"charts.type":_charts} = imports.metadata.plugins.stargazers.inputs({data, account, q}) let {"charts.type": _charts} = imports.metadata.plugins.stargazers.inputs({data, account, q})
//Retrieve stargazers from graphql api //Retrieve stargazers from graphql api
console.debug(`metrics/compute/${login}/plugins > stargazers > querying api`) console.debug(`metrics/compute/${login}/plugins > stargazers > querying api`)
const repositories = data.user.repositories.nodes.map(({name:repository, owner:{login:owner}}) => ({repository, owner})) ?? [] const repositories = data.user.repositories.nodes.map(({name: repository, owner: {login: owner}}) => ({repository, owner})) ?? []
const dates = [] const dates = []
for (const {repository, owner} of repositories) { for (const {repository, owner} of repositories) {
//Iterate through stargazers //Iterate through stargazers
@@ -20,7 +20,7 @@ export default async function({login, graphql, data, imports, q, queries, accoun
let pushed = 0 let pushed = 0
do { do {
console.debug(`metrics/compute/${login}/plugins > stargazers > retrieving stargazers of ${repository} after ${cursor}`) console.debug(`metrics/compute/${login}/plugins > stargazers > retrieving stargazers of ${repository} after ${cursor}`)
const {repository:{stargazers:{edges}}} = await graphql(queries.stargazers({login:owner, repository, after:cursor ? `after: "${cursor}"` : ""})) const {repository: {stargazers: {edges}}} = await graphql(queries.stargazers({login: owner, repository, after: cursor ? `after: "${cursor}"` : ""}))
cursor = edges?.[edges?.length - 1]?.cursor cursor = edges?.[edges?.length - 1]?.cursor
dates.push(...edges.map(({starredAt}) => new Date(starredAt))) dates.push(...edges.map(({starredAt}) => new Date(starredAt)))
pushed = edges.length pushed = edges.length
@@ -32,7 +32,7 @@ export default async function({login, graphql, data, imports, q, queries, accoun
//Compute stargazers increments //Compute stargazers increments
const days = 14 * (1 + data.large / 2) const days = 14 * (1 + data.large / 2)
const increments = {dates:Object.fromEntries([...new Array(days).fill(null).map((_, i) => [new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().slice(0, 10), 0]).reverse()]), max:NaN, min:NaN} const increments = {dates: Object.fromEntries([...new Array(days).fill(null).map((_, i) => [new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().slice(0, 10), 0]).reverse()]), max: NaN, min: NaN}
dates dates
.map(date => date.toISOString().slice(0, 10)) .map(date => date.toISOString().slice(0, 10))
.filter(date => date in increments.dates) .filter(date => date in increments.dates)
@@ -42,12 +42,12 @@ export default async function({login, graphql, data, imports, q, queries, accoun
//Compute total stargazers //Compute total stargazers
let {stargazers} = data.computed.repositories let {stargazers} = data.computed.repositories
const total = {dates:{...increments.dates}, max:NaN, min:NaN} const total = {dates: {...increments.dates}, max: NaN, min: NaN}
{ {
const dates = Object.keys(total.dates) const dates = Object.keys(total.dates)
for (let i = dates.length - 1; i >= 0; i--) { for (let i = dates.length - 1; i >= 0; i--) {
const date = dates[i], tomorrow = dates[i + 1] const date = dates[i], tomorrow = dates[i + 1]
stargazers -= (increments.dates[tomorrow] ?? 0) stargazers -= increments.dates[tomorrow] ?? 0
total.dates[date] = stargazers total.dates[date] = stargazers
} }
} }
@@ -61,22 +61,23 @@ export default async function({login, graphql, data, imports, q, queries, accoun
let charts = null let charts = null
if (_charts === "chartist") { if (_charts === "chartist") {
console.debug(`metrics/compute/${login}/plugins > stargazers > generating charts`) console.debug(`metrics/compute/${login}/plugins > stargazers > generating charts`)
charts = await Promise.all([{data:total, low:total.min, high:total.max}, {data:increments, ref:0, low:increments.min, high:increments.max, sign:true}].map(({data:{dates:set}, high, low, ref, sign = false}) => imports.chartist("line", { charts = await Promise.all([{data: total, low: total.min, high: total.max}, {data: increments, ref: 0, low: increments.min, high: increments.max, sign: true}].map(({data: {dates: set}, high, low, ref, sign = false}) =>
width:480 * (1 + data.large), imports.chartist("line", {
height:160, width: 480 * (1 + data.large),
showPoint:true, height: 160,
axisX:{showGrid:false}, showPoint: true,
axisY:{showLabel:false, offset:20, labelInterpolationFnc:value => imports.format(value, {sign}), high, low, referenceValue:ref}, axisX: {showGrid: false},
showArea:true, axisY: {showLabel: false, offset: 20, labelInterpolationFnc: value => imports.format(value, {sign}), high, low, referenceValue: ref},
fullWidth:true, showArea: true,
fullWidth: true,
}, { }, {
labels:Object.keys(set).map((date, i, a) => { labels: Object.keys(set).map((date, i, a) => {
const day = date.substring(date.length - 2) const day = date.substring(date.length - 2)
if ((i === 0) || ((a[i - 1]) && (date.substring(0, 7) !== a[i - 1].substring(0, 7)))) if ((i === 0) || ((a[i - 1]) && (date.substring(0, 7) !== a[i - 1].substring(0, 7))))
return `${day} ${months[Number(date.substring(5, 7))]}` return `${day} ${months[Number(date.substring(5, 7))]}`
return day return day
}), }),
series:[Object.values(set)], series: [Object.values(set)],
}) })
)) ))
data.postscripts.push(`(${function(format) { data.postscripts.push(`(${function(format) {
@@ -99,6 +100,6 @@ export default async function({login, graphql, data, imports, q, queries, accoun
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -7,7 +7,7 @@ export default async function({login, q, imports, data, account}, {enabled = fal
return null return null
//Load inputs //Load inputs
let {limit, ignored, only, "limit.repositories":_limit, languages, "limit.languages":_limit_languages, "shuffle.repositories":_shuffle} = imports.metadata.plugins.starlists.inputs({data, account, q}) let {limit, ignored, only, "limit.repositories": _limit, languages, "limit.languages": _limit_languages, "shuffle.repositories": _shuffle} = imports.metadata.plugins.starlists.inputs({data, account, q})
ignored = ignored.map(imports.stripemojis) ignored = ignored.map(imports.stripemojis)
only = only.map(imports.stripemojis) only = only.map(imports.stripemojis)
@@ -20,12 +20,13 @@ export default async function({login, q, imports, data, account}, {enabled = fal
//Fetch star lists //Fetch star lists
console.debug(`metrics/compute/${login}/plugins > starlists > fetching lists`) console.debug(`metrics/compute/${login}/plugins > starlists > fetching lists`)
await page.goto(`https://github.com/${login}?tab=stars`) await page.goto(`https://github.com/${login}?tab=stars`)
let lists = (await page.evaluate(login => [...document.querySelectorAll(`[href^='/stars/${login}/lists']`)].map(element => ({ let lists = (await page.evaluate(login =>
link:element.href, [...document.querySelectorAll(`[href^='/stars/${login}/lists']`)].map(element => ({
name:element.querySelector("h3")?.innerText ?? "", link: element.href,
description:element.querySelector("span")?.innerText ?? "", name: element.querySelector("h3")?.innerText ?? "",
count:Number(element.querySelector("div")?.innerText.match(/(?<count>\d+)/)?.groups.count), description: element.querySelector("span")?.innerText ?? "",
repositories:[], count: Number(element.querySelector("div")?.innerText.match(/(?<count>\d+)/)?.groups.count),
repositories: [],
})), login)) })), login))
const count = lists.length const count = lists.length
console.debug(`metrics/compute/${login}/plugins > starlists > found [${lists.map(({name}) => name)}]`) console.debug(`metrics/compute/${login}/plugins > starlists > found [${lists.map(({name}) => name)}]`)
@@ -49,15 +50,16 @@ export default async function({login, q, imports, data, account}, {enabled = fal
console.debug(`metrics/compute/${login}/plugins > starlists > fetching page ${i}`) console.debug(`metrics/compute/${login}/plugins > starlists > fetching page ${i}`)
await page.goto(`${list.link}?page=${i}`) await page.goto(`${list.link}?page=${i}`)
repositories.push( repositories.push(
...await page.evaluate(() => [...document.querySelectorAll("#user-list-repositories > div:not(.paginate-container)")].map(element => ({ ...await page.evaluate(() =>
name:element.querySelector("div:first-child")?.innerText.replace(" / ", "/") ?? "", [...document.querySelectorAll("#user-list-repositories > div:not(.paginate-container)")].map(element => ({
description:element.querySelector(".py-1")?.innerText ?? "", name: element.querySelector("div:first-child")?.innerText.replace(" / ", "/") ?? "",
language:{ description: element.querySelector(".py-1")?.innerText ?? "",
name:element.querySelector("[itemprop='programmingLanguage']")?.innerText ?? "", language: {
color:element.querySelector(".repo-language-color")?.style?.backgroundColor?.match(/\d+/g)?.map(x => Number(x).toString(16).padStart(2, "0")).join("") ?? null, name: element.querySelector("[itemprop='programmingLanguage']")?.innerText ?? "",
color: element.querySelector(".repo-language-color")?.style?.backgroundColor?.match(/\d+/g)?.map(x => Number(x).toString(16).padStart(2, "0")).join("") ?? null,
}, },
stargazers:Number(element.querySelector("[href$='/stargazers']")?.innerText.trim().replace(/[^\d]/g, "") ?? NaN), stargazers: Number(element.querySelector("[href$='/stargazers']")?.innerText.trim().replace(/[^\d]/g, "") ?? NaN),
forks:Number(element.querySelector("[href$='/network/members']")?.innerText.trim().replace(/[^\d]/g, "") ?? NaN), forks: Number(element.querySelector("[href$='/network/members']")?.innerText.trim().replace(/[^\d]/g, "") ?? NaN),
})) }))
), ),
) )
@@ -73,7 +75,7 @@ export default async function({login, q, imports, data, account}, {enabled = fal
//Compute languages statistics //Compute languages statistics
if (languages) { if (languages) {
list.languages = {} list.languages = {}
for (const {language:{name, color}} of repositories) { for (const {language: {name, color}} of repositories) {
if (name) if (name)
list.languages[name] = (list.languages[name] ?? 0) + 1 list.languages[name] = (list.languages[name] ?? 0) + 1
if (color) if (color)
@@ -81,7 +83,7 @@ export default async function({login, q, imports, data, account}, {enabled = fal
} }
list.languages = Object.entries(list.languages).sort((a, b) => b[1] - a[1]).slice(0, _limit_languages || Infinity) list.languages = Object.entries(list.languages).sort((a, b) => b[1] - a[1]).slice(0, _limit_languages || Infinity)
const visible = list.languages.map(([_, value]) => value).reduce((a, b) => a + b, 0) const visible = list.languages.map(([_, value]) => value).reduce((a, b) => a + b, 0)
list.languages = list.languages.map(([name, value]) => ({name, value, color:name in colors ? `#${colors[name]}` : null, x:0, p:value / visible})) list.languages = list.languages.map(([name, value]) => ({name, value, color: name in colors ? `#${colors[name]}` : null, x: 0, p: value / visible}))
for (let i = 1; i < list.languages.length; i++) for (let i = 1; i < list.languages.length; i++)
list.languages[i].x = (list.languages[i - 1]?.x ?? 0) + (list.languages[i - 1]?.value ?? 0) / visible list.languages[i].x = (list.languages[i - 1]?.x ?? 0) + (list.languages[i - 1]?.value ?? 0) / visible
} }
@@ -99,6 +101,6 @@ export default async function({login, q, imports, data, account}, {enabled = fal
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -11,7 +11,7 @@ export default async function({login, data, graphql, q, queries, imports, accoun
//Retrieve user stars from graphql api //Retrieve user stars from graphql api
console.debug(`metrics/compute/${login}/plugins > stars > querying api`) console.debug(`metrics/compute/${login}/plugins > stars > querying api`)
const {user:{starredRepositories:{edges:repositories}}} = await graphql(queries.stars({login, limit})) const {user: {starredRepositories: {edges: repositories}}} = await graphql(queries.stars({login, limit}))
//Format starred repositories //Format starred repositories
for (const edge of repositories) { for (const edge of repositories) {
@@ -32,6 +32,6 @@ export default async function({login, data, graphql, q, queries, imports, accoun
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -10,7 +10,7 @@ export default async function({login, q, imports, data, account}, {enabled = fal
imports.metadata.plugins.stackoverflow.inputs({data, account, q}) imports.metadata.plugins.stackoverflow.inputs({data, account, q})
//Start puppeteer and navigate to github.community //Start puppeteer and navigate to github.community
const result = {stats:{solutions:0, posts:0, topics:0, received:0}, badges:{count:0}} const result = {stats: {solutions: 0, posts: 0, topics: 0, received: 0}, badges: {count: 0}}
console.debug(`metrics/compute/${login}/plugins > support > starting browser`) console.debug(`metrics/compute/${login}/plugins > support > starting browser`)
const browser = await imports.puppeteer.launch() const browser = await imports.puppeteer.launch()
console.debug(`metrics/compute/${login}/plugins > support > started ${await browser.version()}`) console.debug(`metrics/compute/${login}/plugins > support > started ${await browser.version()}`)
@@ -21,10 +21,10 @@ export default async function({login, q, imports, data, account}, {enabled = fal
await page.goto(`https://github.community/u/${login}`) await page.goto(`https://github.community/u/${login}`)
const frame = page.mainFrame() const frame = page.mainFrame()
try { try {
await frame.waitForSelector(".user-profile-names", {timeout:5000}) await frame.waitForSelector(".user-profile-names", {timeout: 5000})
} }
catch { catch {
throw {error:{message:"Could not find matching account on github.community"}} throw {error: {message: "Could not find matching account on github.community"}}
} }
} }
@@ -36,7 +36,8 @@ export default async function({login, q, imports, data, account}, {enabled = fal
Object.assign( Object.assign(
result.stats, result.stats,
Object.fromEntries( Object.fromEntries(
(await frame.evaluate(() => [...document.querySelectorAll(".stats-section li")].map(el => [ (await frame.evaluate(() =>
[...document.querySelectorAll(".stats-section li")].map(el => [
el.querySelector(".label").innerText.trim().toLocaleLowerCase(), el.querySelector(".label").innerText.trim().toLocaleLowerCase(),
el.querySelector(".value").innerText.trim().toLocaleLowerCase(), el.querySelector(".value").innerText.trim().toLocaleLowerCase(),
]) ])
@@ -64,8 +65,8 @@ export default async function({login, q, imports, data, account}, {enabled = fal
const frame = page.mainFrame() const frame = page.mainFrame()
await frame.waitForSelector(".badge-group-list") await frame.waitForSelector(".badge-group-list")
const badges = await frame.evaluate(() => ({ const badges = await frame.evaluate(() => ({
uniques:[...document.querySelectorAll(".badge-card .badge-link")].map(el => el.innerText), uniques: [...document.querySelectorAll(".badge-card .badge-link")].map(el => el.innerText),
multiples:[...document.querySelectorAll(".grant-count")].map(el => Number(el.innerText)), multiples: [...document.querySelectorAll(".grant-count")].map(el => Number(el.innerText)),
})) }))
badges.count = badges.uniques.length + (badges.multiples.reduce((a, b) => a + b, 0) - badges.multiples.length) badges.count = badges.uniques.length + (badges.multiples.reduce((a, b) => a + b, 0) - badges.multiples.length)
result.badges = badges result.badges = badges
@@ -82,6 +83,6 @@ export default async function({login, q, imports, data, account}, {enabled = fal
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -8,7 +8,7 @@ export default async function({login, data, imports, q, account}, {enabled = fal
//Load inputs //Load inputs
let {sort, mode, limit} = imports.metadata.plugins.topics.inputs({data, account, q}) let {sort, mode, limit} = imports.metadata.plugins.topics.inputs({data, account, q})
const type = {starred:"labels", labels:"labels", mastered:"icons", icons:"icons"}[mode] const type = {starred: "labels", labels: "labels", mastered: "icons", icons: "icons"}[mode]
const shuffle = (sort === "random") const shuffle = (sort === "random")
//Start puppeteer and navigate to topics //Start puppeteer and navigate to topics
@@ -27,10 +27,11 @@ export default async function({login, data, imports, q, account}, {enabled = fal
const frame = page.mainFrame() const frame = page.mainFrame()
//Extract topics //Extract topics
await Promise.race([frame.waitForSelector("ul.repo-list"), frame.waitForSelector(".blankslate")]) await Promise.race([frame.waitForSelector("ul.repo-list"), frame.waitForSelector(".blankslate")])
const starred = await frame.evaluate(() => [...document.querySelectorAll("ul.repo-list li")].map(li => ({ const starred = await frame.evaluate(() =>
name:li.querySelector(".f3").innerText, [...document.querySelectorAll("ul.repo-list li")].map(li => ({
description:li.querySelector(".f5").innerText, name: li.querySelector(".f3").innerText,
icon:li.querySelector("img")?.src ?? null, description: li.querySelector(".f5").innerText,
icon: li.querySelector("img")?.src ?? null,
})) }))
) )
console.debug(`metrics/compute/${login}/plugins > topics > extracted ${starred.length} starred topics`) console.debug(`metrics/compute/${login}/plugins > topics > extracted ${starred.length} starred topics`)
@@ -57,7 +58,7 @@ export default async function({login, data, imports, q, account}, {enabled = fal
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`) console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
const removed = topics.splice(limit) const removed = topics.splice(limit)
if (removed.length) if (removed.length)
topics.push({name:`And ${removed.length} more...`, description:removed.map(({name}) => name).join(", "), icon:null}) topics.push({name: `And ${removed.length} more...`, description: removed.map(({name}) => name).join(", "), icon: null})
} }
//Convert icons to base64 //Convert icons to base64
@@ -67,7 +68,7 @@ export default async function({login, data, imports, q, account}, {enabled = fal
console.debug(`metrics/compute/${login}/plugins > topics > processing ${topic.name}`) console.debug(`metrics/compute/${login}/plugins > topics > processing ${topic.name}`)
const {icon} = topic const {icon} = topic
topic.icon = await imports.imgb64(icon) topic.icon = await imports.imgb64(icon)
topic.icon24 = await imports.imgb64(icon, {force:true, width:24, height:24}) topic.icon24 = await imports.imgb64(icon, {force: true, width: 24, height: 24})
} }
//Escape HTML description //Escape HTML description
topic.description = imports.htmlescape(topic.description) topic.description = imports.htmlescape(topic.description)
@@ -86,12 +87,12 @@ export default async function({login, data, imports, q, account}, {enabled = fal
} }
//Results //Results
return {mode, type, list:topics} return {mode, type, list: topics}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
if (error.error?.message) if (error.error?.message)
throw error throw error
throw {error:{message:"An error occured", instance:error}} throw {error: {message: "An error occured", instance: error}}
} }
} }

View File

@@ -11,17 +11,18 @@ export default async function({login, imports, data, rest, q, account}, {enabled
skipped.push(...data.shared["repositories.skipped"]) skipped.push(...data.shared["repositories.skipped"])
//Repositories //Repositories
const repositories = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})) ?? [] const repositories = data.user.repositories.nodes.map(({name: repo, owner: {login: owner}}) => ({repo, owner})) ?? []
//Get views stats from repositories //Get views stats from repositories
console.debug(`metrics/compute/${login}/plugins > traffic > querying api`) console.debug(`metrics/compute/${login}/plugins > traffic > querying api`)
const views = {count:0, uniques:0} const views = {count: 0, uniques: 0}
const response = [...await Promise.allSettled(repositories.map(({repo, owner}) => (skipped.includes(repo.toLocaleLowerCase())) || (skipped.includes(`${owner}/${repo}`.toLocaleLowerCase())) ? {} : rest.repos.getViews({owner, repo})))].filter(({status}) => status === "fulfilled" const response = [...await Promise.allSettled(repositories.map(({repo, owner}) => (skipped.includes(repo.toLocaleLowerCase())) || (skipped.includes(`${owner}/${repo}`.toLocaleLowerCase())) ? {} : rest.repos.getViews({owner, repo})))].filter(({status}) =>
status === "fulfilled"
).map(({value}) => value) ).map(({value}) => value)
//Compute views //Compute views
console.debug(`metrics/compute/${login}/plugins > traffic > computing stats`) console.debug(`metrics/compute/${login}/plugins > traffic > computing stats`)
response.filter(({data}) => data).map(({data:{count, uniques}}) => (views.count += count, views.uniques += uniques)) response.filter(({data}) => data).map(({data: {count, uniques}}) => (views.count += count, views.uniques += uniques))
//Results //Results
return {views} return {views}
@@ -31,6 +32,6 @@ export default async function({login, imports, data, rest, q, account}, {enabled
let message = "An error occured" let message = "An error occured"
if (error.status === 403) if (error.status === 403)
message = "Insufficient token rights" message = "Insufficient token rights"
throw {error:{message, instance:error}} throw {error: {message, instance: error}}
} }
} }

View File

@@ -7,11 +7,11 @@ export default async function({login, imports, data, q, account}, {enabled = fal
return null return null
//Load inputs //Load inputs
let {limit, user:username, attachments} = imports.metadata.plugins.tweets.inputs({data, account, q}) let {limit, user: username, attachments} = imports.metadata.plugins.tweets.inputs({data, account, q})
//Load user profile //Load user profile
console.debug(`metrics/compute/${login}/plugins > tweets > loading twitter profile (@${username})`) 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}`}}) const {data: {data: profile = null}} = await imports.axios.get(`https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url,verified`, {headers: {Authorization: `Bearer ${token}`}})
//Load profile image //Load profile image
if (profile?.profile_image_url) { if (profile?.profile_image_url) {
@@ -21,9 +21,9 @@ export default async function({login, imports, data, q, account}, {enabled = fal
//Load tweets //Load tweets
console.debug(`metrics/compute/${login}/plugins > tweets > querying api`) console.debug(`metrics/compute/${login}/plugins > tweets > querying api`)
const {data:{data:tweets = [], includes:{media = []} = {}}} = await imports.axios.get( const {data: {data: tweets = [], includes: {media = []} = {}}} = await imports.axios.get(
`https://api.twitter.com/2/tweets/search/recent?query=from:${username}&tweet.fields=created_at,entities&media.fields=preview_image_url,url,type&expansions=entities.mentions.username,attachments.media_keys`, `https://api.twitter.com/2/tweets/search/recent?query=from:${username}&tweet.fields=created_at,entities&media.fields=preview_image_url,url,type&expansions=entities.mentions.username,attachments.media_keys`,
{headers:{Authorization:`Bearer ${token}`}}, {headers: {Authorization: `Bearer ${token}`}},
) )
const medias = new Map(media.map(({media_key, type, url, preview_image_url}) => [media_key, (type === "photo") || (type === "animated_gif") ? url : type === "video" ? preview_image_url : null])) const medias = new Map(media.map(({media_key, type, url, preview_image_url}) => [media_key, (type === "photo") || (type === "animated_gif") ? url : type === "video" ? preview_image_url : null]))
@@ -37,7 +37,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
await Promise.all(tweets.map(async tweet => { await Promise.all(tweets.map(async tweet => {
//Mentions and urls //Mentions and urls
tweet.mentions = tweet.entities?.mentions?.map(({username}) => username) ?? [] tweet.mentions = tweet.entities?.mentions?.map(({username}) => username) ?? []
tweet.urls = new Map(tweet.entities?.urls?.map(({url, display_url:link}) => [url, link]) ?? []) tweet.urls = new Map(tweet.entities?.urls?.map(({url, display_url: link}) => [url, link]) ?? [])
//Attachments //Attachments
if (attachments) { if (attachments) {
//Retrieve linked content //Retrieve linked content
@@ -48,31 +48,31 @@ export default async function({login, imports, data, q, account}, {enabled = fal
} }
//Medias //Medias
if (tweet.attachments) if (tweet.attachments)
tweet.attachments = await Promise.all(tweet.attachments.media_keys.filter(key => medias.get(key)).map(key => medias.get(key)).map(async url => ({image:await imports.imgb64(url, {height:-1, width:450})}))) tweet.attachments = await Promise.all(tweet.attachments.media_keys.filter(key => medias.get(key)).map(key => medias.get(key)).map(async url => ({image: await imports.imgb64(url, {height: -1, width: 450})})))
if (linked) { if (linked) {
const {result:{ogImage, ogSiteName:website, ogTitle:title, ogDescription:description}} = await imports.opengraph({url:linked}) const {result: {ogImage, ogSiteName: website, ogTitle: title, ogDescription: description}} = await imports.opengraph({url: linked})
const image = await imports.imgb64(ogImage?.url, {height:-1, width:450, fallback:false}) const image = await imports.imgb64(ogImage?.url, {height: -1, width: 450, fallback: false})
if (image) { if (image) {
if (tweet.attachments) if (tweet.attachments)
tweet.attachments.unshift([{image, title, description, website}]) tweet.attachments.unshift([{image, title, description, website}])
else else
tweet.attachments = [{image, title, description, website}] tweet.attachments = [{image, title, description, website}]
} }
else else {
tweet.text = `${tweet.text}\n${linked}` tweet.text = `${tweet.text}\n${linked}`
}
} }
} }
else else {
tweet.attachments = null tweet.attachments = null
}
//Format text //Format text
console.debug(`metrics/compute/${login}/plugins > tweets > formatting tweet ${tweet.id}`) console.debug(`metrics/compute/${login}/plugins > tweets > formatting tweet ${tweet.id}`)
tweet.createdAt = `${imports.format.date(tweet.created_at, {time:true})} on ${imports.format.date(tweet.created_at, {date:true})}` tweet.createdAt = `${imports.format.date(tweet.created_at, {time: true})} on ${imports.format.date(tweet.created_at, {date: true})}`
tweet.text = imports.htmlescape( tweet.text = imports.htmlescape(
//Escape tags //Escape tags
imports.htmlescape(tweet.text, {"<":true, ">":true}) imports.htmlescape(tweet.text, {"<": true, ">": true})
//Mentions //Mentions
.replace(new RegExp(`@(${tweet.mentions.join("|")})`, "gi"), '<span class="mention">@$1</span>') .replace(new RegExp(`@(${tweet.mentions.join("|")})`, "gi"), '<span class="mention">@$1</span>')
//Hashtags (this regex comes from the twitter source code) //Hashtags (this regex comes from the twitter source code)
@@ -84,12 +84,12 @@ export default async function({login, imports, data, q, account}, {enabled = fal
.replace(/\n/g, "<br/>") .replace(/\n/g, "<br/>")
//Links //Links
.replace(new RegExp(`${tweet.urls.size ? "" : "noop^"}(${[...tweet.urls.keys()].map(url => `(?:${url})`).join("|")})`, "gi"), (_, url) => `<a href="${url}" class="link">${tweet.urls.get(url)}</a>`), .replace(new RegExp(`${tweet.urls.size ? "" : "noop^"}(${[...tweet.urls.keys()].map(url => `(?:${url})`).join("|")})`, "gi"), (_, url) => `<a href="${url}" class="link">${tweet.urls.get(url)}</a>`),
{"&":true}, {"&": true},
) )
})) }))
//Result //Result
return {username, profile, list:tweets} return {username, profile, list: tweets}
} }
//Handle errors //Handle errors
catch (error) { catch (error) {
@@ -100,6 +100,6 @@ export default async function({login, imports, data, q, account}, {enabled = fal
message = `API returned ${status}${description ? ` (${description})` : ""}` message = `API returned ${status}${description ? ` (${description})` : ""}`
error = error.response?.data ?? null error = error.response?.data ?? null
} }
throw {error:{message, instance:error}} throw {error: {message, instance: error}}
} }
} }

View File

@@ -7,30 +7,30 @@ export default async function({login, q, imports, data, account}, {enabled = fal
return null return null
//Load inputs //Load inputs
let {sections, days, limit, url, user, "languages.other":others} = imports.metadata.plugins.wakatime.inputs({data, account, q}) let {sections, days, limit, url, user, "languages.other": others} = imports.metadata.plugins.wakatime.inputs({data, account, q})
if (!limit) if (!limit)
limit = void limit limit = void limit
const range = { const range = {
"7":"last_7_days", "7": "last_7_days",
"30":"last_30_days", "30": "last_30_days",
"180":"last_6_months", "180": "last_6_months",
"365":"last_year", "365": "last_year",
}[days] ?? "last_7_days" }[days] ?? "last_7_days"
//Querying api and format result (https://wakatime.com/developers#stats) //Querying api and format result (https://wakatime.com/developers#stats)
console.debug(`metrics/compute/${login}/plugins > wakatime > querying api`) console.debug(`metrics/compute/${login}/plugins > wakatime > querying api`)
const {data:{data:stats}} = await imports.axios.get(`${url}/api/v1/users/${user}/stats/${range}?api_key=${token}`) const {data: {data: stats}} = await imports.axios.get(`${url}/api/v1/users/${user}/stats/${range}?api_key=${token}`)
const result = { const result = {
sections, sections,
days, days,
time:{ time: {
total:(others ? stats.total_seconds_including_other_language : stats.total_seconds) / (60 * 60), total: (others ? stats.total_seconds_including_other_language : stats.total_seconds) / (60 * 60),
daily:(others ? stats.daily_average_including_other_language : stats.daily_average) / (60 * 60), daily: (others ? stats.daily_average_including_other_language : stats.daily_average) / (60 * 60),
}, },
projects:stats.projects?.map(({name, percent, total_seconds:total}) => ({name, percent:percent / 100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit), projects: stats.projects?.map(({name, percent, total_seconds: total}) => ({name, percent: percent / 100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit),
languages:stats.languages?.map(({name, percent, total_seconds:total}) => ({name, percent:percent / 100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit), languages: stats.languages?.map(({name, percent, total_seconds: total}) => ({name, percent: percent / 100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit),
os:stats.operating_systems?.map(({name, percent, total_seconds:total}) => ({name, percent:percent / 100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit), os: stats.operating_systems?.map(({name, percent, total_seconds: total}) => ({name, percent: percent / 100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit),
editors:stats.editors?.map(({name, percent, total_seconds:total}) => ({name, percent:percent / 100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit), editors: stats.editors?.map(({name, percent, total_seconds: total}) => ({name, percent: percent / 100, total})).sort((a, b) => b.percent - a.percent).slice(0, limit),
} }
//Result //Result
@@ -44,6 +44,6 @@ export default async function({login, q, imports, data, account}, {enabled = fal
message = `API returned ${status}` message = `API returned ${status}`
error = error.response?.data ?? null error = error.response?.data ?? null
} }
throw {error:{message, instance:error}} throw {error: {message, instance: error}}
} }
} }

View File

@@ -6,48 +6,48 @@ export default async function(_, {data}, {imports}) {
const {user, computed, plugins} = data const {user, computed, plugins} = data
Object.assign(data, { Object.assign(data, {
//Base //Base
NAME:user.name, NAME: user.name,
LOGIN:user.login, LOGIN: user.login,
REGISTRATION_DATE:user.createdAt, REGISTRATION_DATE: user.createdAt,
REGISTERED_YEARS:computed.registered.years, REGISTERED_YEARS: computed.registered.years,
LOCATION:user.location, LOCATION: user.location,
WEBSITE:user.websiteUrl, WEBSITE: user.websiteUrl,
REPOSITORIES:user.repositories.totalCount, REPOSITORIES: user.repositories.totalCount,
REPOSITORIES_DISK_USAGE:user.repositories.totalDiskUsage, REPOSITORIES_DISK_USAGE: user.repositories.totalDiskUsage,
PACKAGES:user.packages.totalCount, PACKAGES: user.packages.totalCount,
STARRED:user.starredRepositories.totalCount, STARRED: user.starredRepositories.totalCount,
WATCHING:user.watching.totalCount, WATCHING: user.watching.totalCount,
SPONSORING:user.sponsorshipsAsSponsor.totalCount, SPONSORING: user.sponsorshipsAsSponsor.totalCount,
SPONSORS:user.sponsorshipsAsMaintainer.totalCount, SPONSORS: user.sponsorshipsAsMaintainer.totalCount,
REPOSITORIES_CONTRIBUTED_TO:user.repositoriesContributedTo.totalCount, REPOSITORIES_CONTRIBUTED_TO: user.repositoriesContributedTo.totalCount,
COMMITS:computed.commits, COMMITS: computed.commits,
COMMITS_PUBLIC:user.contributionsCollection.totalCommitContributions, COMMITS_PUBLIC: user.contributionsCollection.totalCommitContributions,
COMMITS_PRIVATE:user.contributionsCollection.restrictedContributionsCount, COMMITS_PRIVATE: user.contributionsCollection.restrictedContributionsCount,
ISSUES:user.contributionsCollection.totalIssueContributions, ISSUES: user.contributionsCollection.totalIssueContributions,
PULL_REQUESTS:user.contributionsCollection.totalPullRequestContributions, PULL_REQUESTS: user.contributionsCollection.totalPullRequestContributions,
PULL_REQUESTS_REVIEWS:user.contributionsCollection.totalPullRequestReviewContributions, PULL_REQUESTS_REVIEWS: user.contributionsCollection.totalPullRequestReviewContributions,
FOLLOWERS:user.followers.totalCount, FOLLOWERS: user.followers.totalCount,
FOLLOWING:user.following.totalCount, FOLLOWING: user.following.totalCount,
ISSUE_COMMENTS:user.issueComments.totalCount, ISSUE_COMMENTS: user.issueComments.totalCount,
ORGANIZATIONS:user.organizations.totalCount, ORGANIZATIONS: user.organizations.totalCount,
WATCHERS:computed.repositories.watchers, WATCHERS: computed.repositories.watchers,
STARGAZERS:computed.repositories.stargazers, STARGAZERS: computed.repositories.stargazers,
FORKS:computed.repositories.forks, FORKS: computed.repositories.forks,
RELEASES:computed.repositories.releases, RELEASES: computed.repositories.releases,
VERSION:data.meta.version, VERSION: data.meta.version,
//Lines //Lines
LINES_ADDED:plugins.lines?.added ?? 0, LINES_ADDED: plugins.lines?.added ?? 0,
LINES_DELETED:plugins.lines?.deleted ?? 0, LINES_DELETED: plugins.lines?.deleted ?? 0,
//Gists //Gists
GISTS:plugins.gists?.totalCount ?? 0, GISTS: plugins.gists?.totalCount ?? 0,
GISTS_STARGAZERS:plugins.gists?.stargazers ?? 0, GISTS_STARGAZERS: plugins.gists?.stargazers ?? 0,
//Languages //Languages
LANGUAGES:plugins.languages?.favorites?.map(({name, value, size, color}) => ({name, value, size, color})) ?? [], LANGUAGES: plugins.languages?.favorites?.map(({name, value, size, color}) => ({name, value, size, color})) ?? [],
//Posts //Posts
POSTS:plugins.posts?.list ?? [], POSTS: plugins.posts?.list ?? [],
//Tweets //Tweets
TWEETS:plugins.tweets?.list ?? [], TWEETS: plugins.tweets?.list ?? [],
//Topics //Topics
TOPICS:plugins.topics?.list ?? [], TOPICS: plugins.topics?.list ?? [],
}) })
} }

View File

@@ -4,19 +4,19 @@ export default async function({login, q}, {data, rest, graphql, queries, account
const {repo} = q const {repo} = q
if (!repo) { if (!repo) {
console.debug(`metrics/compute/${login}/${repo} > error, repo was undefined`) console.debug(`metrics/compute/${login}/${repo} > error, repo was undefined`)
data.errors.push({error:{message:'You must pass a "repo" argument to use this template'}}) data.errors.push({error: {message: 'You must pass a "repo" argument to use this template'}})
return imports.plugins.core(...arguments) return imports.plugins.core(...arguments)
} }
console.debug(`metrics/compute/${login}/${repo} > switching to mode ${account}`) console.debug(`metrics/compute/${login}/${repo} > switching to mode ${account}`)
//Retrieving single repository //Retrieving single repository
console.debug(`metrics/compute/${login}/${repo} > retrieving single repository ${repo}`) console.debug(`metrics/compute/${login}/${repo} > retrieving single repository ${repo}`)
const {[account === "bypass" ? "user" : account]:{repository}} = await graphql(queries.base.repository({login, repo, account})) const {[account === "bypass" ? "user" : account]: {repository}} = await graphql(queries.base.repository({login, repo, account}))
data.user.repositories.nodes = [repository] data.user.repositories.nodes = [repository]
data.repo = repository data.repo = repository
//Contributors and sponsors //Contributors and sponsors
data.repo.contributors = {totalCount:(await rest.repos.listContributors({owner:data.repo.owner.login, repo})).data.length} data.repo.contributors = {totalCount: (await rest.repos.listContributors({owner: data.repo.owner.login, repo})).data.length}
data.repo.sponsorshipsAsMaintainer = data.user.sponsorshipsAsMaintainer data.repo.sponsorshipsAsMaintainer = data.user.sponsorshipsAsMaintainer
//Get commit activity //Get commit activity
@@ -25,7 +25,7 @@ export default async function({login, q}, {data, rest, graphql, queries, account
for (let page = 1; page < 100; page++) { for (let page = 1; page < 100; page++) {
console.debug(`metrics/compute/${login}/${repo} > loading page ${page}`) console.debug(`metrics/compute/${login}/${repo} > loading page ${page}`)
try { try {
const {data} = await rest.repos.listCommits({owner:login, repo, per_page:100, page}) const {data} = await rest.repos.listCommits({owner: login, repo, per_page: 100, page})
if (!data.length) { if (!data.length) {
console.debug(`metrics/compute/${login}/${repo} > no more page to load`) console.debug(`metrics/compute/${login}/${repo} > no more page to load`)
break break
@@ -58,16 +58,16 @@ export default async function({login, q}, {data, rest, graphql, queries, account
calendar.splice(days) calendar.splice(days)
const max = Math.max(...calendar) const max = Math.max(...calendar)
//Override contributions calendar //Override contributions calendar
data.user.calendar.contributionCalendar.weeks = calendar.map(commit => ({contributionDays:{color:commit ? `var(--color-calendar-graph-day-L${Math.ceil(commit / max / 0.25)}-bg)` : "var(--color-calendar-graph-day-bg)"}})) data.user.calendar.contributionCalendar.weeks = calendar.map(commit => ({contributionDays: {color: commit ? `var(--color-calendar-graph-day-L${Math.ceil(commit / max / 0.25)}-bg)` : "var(--color-calendar-graph-day-bg)"}}))
//Override plugins parameters //Override plugins parameters
q["projects.limit"] = 0 q["projects.limit"] = 0
//Fetching users count if it's an action //Fetching users count if it's an action
try { try {
if (await rest.repos.getContent({owner:login, repo, path:"action.yml"})) { if (await rest.repos.getContent({owner: login, repo, path: "action.yml"})) {
console.debug(`metrics/compute/${login}/${repo} > this repository seems to be a GitHub action, fetching users using code search`) console.debug(`metrics/compute/${login}/${repo} > this repository seems to be a GitHub action, fetching users using code search`)
const {data:{total_count}} = await rest.search.code({q:`uses ${login} ${repo} path:.github/workflows language:YAML`}) const {data: {total_count}} = await rest.search.code({q: `uses ${login} ${repo} path:.github/workflows language:YAML`})
data.repo.actionUsersCount = total_count data.repo.actionUsersCount = total_count
} }
} }

View File

@@ -10,13 +10,13 @@ const ejs = require("ejs")
//Github action //Github action
const action = yaml.load(fs.readFileSync(path.join(__dirname, "../action.yml"), "utf8")) const action = yaml.load(fs.readFileSync(path.join(__dirname, "../action.yml"), "utf8"))
action.defaults = Object.fromEntries(Object.entries(action.inputs).map(([key, { default: value }]) => [key, value])) action.defaults = Object.fromEntries(Object.entries(action.inputs).map(([key, {default: value}]) => [key, value]))
action.input = vars => Object.fromEntries([...Object.entries(action.defaults), ...Object.entries(vars)].map(([key, value]) => [`INPUT_${key.toLocaleUpperCase()}`, value])) action.input = vars => Object.fromEntries([...Object.entries(action.defaults), ...Object.entries(vars)].map(([key, value]) => [`INPUT_${key.toLocaleUpperCase()}`, value]))
action.run = async vars => action.run = async vars =>
await new Promise((solve, reject) => { await new Promise((solve, reject) => {
let [stdout, stderr] = ["", ""] let [stdout, stderr] = ["", ""]
const env = { ...process.env, ...action.input(vars), GITHUB_REPOSITORY: "lowlighter/metrics" } const env = {...process.env, ...action.input(vars), GITHUB_REPOSITORY: "lowlighter/metrics"}
const child = processes.spawn("node", ["source/app/action/index.mjs"], { env }) const child = processes.spawn("node", ["source/app/action/index.mjs"], {env})
child.stdout.on("data", data => stdout += data) child.stdout.on("data", data => stdout += data)
child.stderr.on("data", data => stderr += data) child.stderr.on("data", data => stderr += data)
child.on("close", code => { child.on("close", code => {
@@ -33,7 +33,7 @@ web.run = async vars => (await axios(`http://localhost:3000/lowlighter?${new url
web.start = async () => web.start = async () =>
new Promise(solve => { new Promise(solve => {
let stdout = "" let stdout = ""
web.instance = processes.spawn("node", ["source/app/web/index.mjs"], { env: { ...process.env, SANDBOX: true } }) web.instance = processes.spawn("node", ["source/app/web/index.mjs"], {env: {...process.env, SANDBOX: true}})
web.instance.stdout.on("data", data => (stdout += data, /Server ready !/.test(stdout) ? solve() : null)) web.instance.stdout.on("data", data => (stdout += data, /Server ready !/.test(stdout) ? solve() : null))
web.instance.stderr.on("data", data => console.error(`${data}`)) web.instance.stderr.on("data", data => console.error(`${data}`))
}) })
@@ -58,8 +58,8 @@ placeholder.run = async vars => {
const config = Object.fromEntries(Object.entries(options).filter(([key]) => /^config[.]/.test(key))) const config = Object.fromEntries(Object.entries(options).filter(([key]) => /^config[.]/.test(key)))
const base = Object.fromEntries(Object.entries(options).filter(([key]) => /^base[.]/.test(key))) const base = Object.fromEntries(Object.entries(options).filter(([key]) => /^base[.]/.test(key)))
return typeof await placeholder({ return typeof await placeholder({
templates: { selected: vars.template }, templates: {selected: vars.template},
plugins: { enabled: { ...enabled, base }, options }, plugins: {enabled: {...enabled, base}, options},
config, config,
version: "TEST", version: "TEST",
user: "lowlighter", user: "lowlighter",
@@ -70,7 +70,7 @@ placeholder.run = async vars => {
//Setup //Setup
beforeAll(async () => { beforeAll(async () => {
//Clean community template //Clean community template
await fs.promises.rm(path.join(__dirname, "../source/templates/@classic"), { recursive: true, force: true }) await fs.promises.rm(path.join(__dirname, "../source/templates/@classic"), {recursive: true, force: true})
//Start web instance //Start web instance
await web.start() await web.start()
}) })
@@ -79,7 +79,7 @@ afterAll(async () => {
//Stop web instance //Stop web instance
await web.stop() await web.stop()
//Clean community template //Clean community template
await fs.promises.rm(path.join(__dirname, "../source/templates/@classic"), { recursive: true, force: true }) await fs.promises.rm(path.join(__dirname, "../source/templates/@classic"), {recursive: true, force: true})
}) })
//Load metadata (as jest doesn't support ESM modules, we use this dirty hack) //Load metadata (as jest doesn't support ESM modules, we use this dirty hack)
@@ -98,11 +98,11 @@ for (const type of ["plugins", "templates"]) {
for (const name in metadata[type]) { for (const name in metadata[type]) {
const cases = yaml const cases = yaml
.load(fs.readFileSync(path.join(__dirname, "../tests/cases", `${name}.${type.replace(/s$/, "")}.yml`), "utf8")) .load(fs.readFileSync(path.join(__dirname, "../tests/cases", `${name}.${type.replace(/s$/, "")}.yml`), "utf8"))
?.map(({ name: test, with: inputs, modes = [], timeout }) => { ?.map(({name: test, with: inputs, modes = [], timeout}) => {
const skip = new Set(Object.entries(metadata.templates).filter(([_, { readme: { compatibility } }]) => !compatibility[name]).map(([template]) => template)) const skip = new Set(Object.entries(metadata.templates).filter(([_, {readme: {compatibility}}]) => !compatibility[name]).map(([template]) => template))
if (!(metadata[type][name].supports?.includes("repository"))) if (!(metadata[type][name].supports?.includes("repository")))
skip.add("repository") skip.add("repository")
return [test, inputs, { skip: [...skip], modes, timeout }] return [test, inputs, {skip: [...skip], modes, timeout}]
}) ?? [] }) ?? []
tests.push(...cases) tests.push(...cases)
} }
@@ -113,13 +113,13 @@ describe("GitHub Action", () =>
describe.each([ describe.each([
["classic", {}], ["classic", {}],
["terminal", {}], ["terminal", {}],
["repository", { repo: "metrics" }], ["repository", {repo: "metrics"}],
])("Template : %s", (template, query) => { ])("Template : %s", (template, query) => {
for (const [name, input, { skip = [], modes = [], timeout } = {}] of tests) { for (const [name, input, {skip = [], modes = [], timeout} = {}] of tests) {
if ((skip.includes(template)) || ((modes.length) && (!modes.includes("action")))) if ((skip.includes(template)) || ((modes.length) && (!modes.includes("action"))))
test.skip(name, () => null) test.skip(name, () => null)
else else
test(name, async () => expect(await action.run({ template, base: "", query: JSON.stringify(query), plugins_errors_fatal: true, dryrun: true, use_mocked_data: true, verify: true, ...input })).toBe(true), timeout) test(name, async () => expect(await action.run({template, base: "", query: JSON.stringify(query), plugins_errors_fatal: true, dryrun: true, use_mocked_data: true, verify: true, ...input})).toBe(true), timeout)
} }
})) }))
@@ -127,13 +127,13 @@ describe("Web instance", () =>
describe.each([ describe.each([
["classic", {}], ["classic", {}],
["terminal", {}], ["terminal", {}],
["repository", { repo: "metrics" }], ["repository", {repo: "metrics"}],
])("Template : %s", (template, query) => { ])("Template : %s", (template, query) => {
for (const [name, input, { skip = [], modes = [], timeout } = {}] of tests) { for (const [name, input, {skip = [], modes = [], timeout} = {}] of tests) {
if ((skip.includes(template)) || ((modes.length) && (!modes.includes("web")))) if ((skip.includes(template)) || ((modes.length) && (!modes.includes("web"))))
test.skip(name, () => null) test.skip(name, () => null)
else else
test(name, async () => expect(await web.run({ template, base: 0, ...query, plugins_errors_fatal: true, verify: true, ...input })).toBe(true), timeout) test(name, async () => expect(await web.run({template, base: 0, ...query, plugins_errors_fatal: true, verify: true, ...input})).toBe(true), timeout)
} }
})) }))
@@ -142,10 +142,10 @@ describe("Web instance (placeholder)", () =>
["classic", {}], ["classic", {}],
["terminal", {}], ["terminal", {}],
])("Template : %s", (template, query) => { ])("Template : %s", (template, query) => {
for (const [name, input, { skip = [], modes = [], timeout } = {}] of tests) { for (const [name, input, {skip = [], modes = [], timeout} = {}] of tests) {
if ((skip.includes(template)) || ((modes.length) && (!modes.includes("placeholder")))) if ((skip.includes(template)) || ((modes.length) && (!modes.includes("placeholder"))))
test.skip(name, () => null) test.skip(name, () => null)
else else
test(name, async () => expect(await placeholder.run({ template, base: 0, ...query, ...input })).toBe(true), timeout) test(name, async () => expect(await placeholder.run({template, base: 0, ...query, ...input})).toBe(true), timeout)
} }
})) }))

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url, options, login = faker.internet.userName() }) { export default function({faker, url, options, login = faker.internet.userName()}) {
//Last.fm api //Last.fm api
if (/^https:..ws.audioscrobbler.com.*$/.test(url)) { if (/^https:..ws.audioscrobbler.com.*$/.test(url)) {
//Get recently played tracks //Get recently played tracks

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url }) { export default function({faker, url}) {
//Last.fm api //Last.fm api
if (/^https:..testapp.herokuapp.com.*$/.test(url)) { if (/^https:..testapp.herokuapp.com.*$/.test(url)) {
//Get Nightscout Data //Get Nightscout Data
@@ -12,8 +12,8 @@ export default function({ faker, url }) {
device: "xDrip-DexcomG5", device: "xDrip-DexcomG5",
date: lastInterval, date: lastInterval,
dateString: new Date(lastInterval).toISOString(), dateString: new Date(lastInterval).toISOString(),
sgv: faker.datatype.number({ min: 40, max: 400 }), sgv: faker.datatype.number({min: 40, max: 400}),
delta: faker.datatype.number({ min: -10, max: 10 }), delta: faker.datatype.number({min: -10, max: 10}),
direction: faker.random.arrayElement(["SingleUp", "DoubleUp", "FortyFiveUp", "Flat", "FortyFiveDown", "SingleDown", "DoubleDown"]), direction: faker.random.arrayElement(["SingleUp", "DoubleUp", "FortyFiveUp", "Flat", "FortyFiveDown", "SingleDown", "DoubleDown"]),
type: "sgv", type: "sgv",
filtered: 0, filtered: 0,
@@ -21,7 +21,7 @@ export default function({ faker, url }) {
rssi: 100, rssi: 100,
noise: 1, noise: 1,
sysTime: new Date(lastInterval).toISOString(), sysTime: new Date(lastInterval).toISOString(),
utcOffset: faker.datatype.number({ min: -12, max: 14 }) * 60, utcOffset: faker.datatype.number({min: -12, max: 14}) * 60,
})), })),
}) })
} }

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url, options, login = faker.internet.userName() }) { export default function({faker, url, options, login = faker.internet.userName()}) {
//Tested url //Tested url
const tested = url.match(/&url=(?<tested>.*?)(?:&|$)/)?.groups?.tested ?? faker.internet.url() const tested = url.match(/&url=(?<tested>.*?)(?:&|$)/)?.groups?.tested ?? faker.internet.url()
//Pagespeed api //Pagespeed api
@@ -43,12 +43,12 @@ export default function({ faker, url, options, login = faker.internet.userName()
maxPotentialFID: faker.datatype.number(500), maxPotentialFID: faker.datatype.number(500),
observedLoad: faker.datatype.number(500), observedLoad: faker.datatype.number(500),
firstMeaningfulPaint: faker.datatype.number(500), firstMeaningfulPaint: faker.datatype.number(500),
observedCumulativeLayoutShift: faker.datatype.float({ max: 1 }), observedCumulativeLayoutShift: faker.datatype.float({max: 1}),
observedSpeedIndex: faker.datatype.number(1000), observedSpeedIndex: faker.datatype.number(1000),
observedSpeedIndexTs: faker.time.recent(), observedSpeedIndexTs: faker.time.recent(),
observedTimeOriginTs: faker.time.recent(), observedTimeOriginTs: faker.time.recent(),
observedLargestContentfulPaint: faker.datatype.number(1000), observedLargestContentfulPaint: faker.datatype.number(1000),
cumulativeLayoutShift: faker.datatype.float({ max: 1 }), cumulativeLayoutShift: faker.datatype.float({max: 1}),
observedFirstPaintTs: faker.time.recent(), observedFirstPaintTs: faker.time.recent(),
observedTraceEndTs: faker.time.recent(), observedTraceEndTs: faker.time.recent(),
largestContentfulPaint: faker.datatype.number(2000), largestContentfulPaint: faker.datatype.number(2000),
@@ -78,22 +78,22 @@ export default function({ faker, url, options, login = faker.internet.userName()
"best-practices": { "best-practices": {
id: "best-practices", id: "best-practices",
title: "Best Practices", title: "Best Practices",
score: faker.datatype.float({ max: 1 }), score: faker.datatype.float({max: 1}),
}, },
seo: { seo: {
id: "seo", id: "seo",
title: "SEO", title: "SEO",
score: faker.datatype.float({ max: 1 }), score: faker.datatype.float({max: 1}),
}, },
accessibility: { accessibility: {
id: "accessibility", id: "accessibility",
title: "Accessibility", title: "Accessibility",
score: faker.datatype.float({ max: 1 }), score: faker.datatype.float({max: 1}),
}, },
performance: { performance: {
id: "performance", id: "performance",
title: "Performance", title: "Performance",
score: faker.datatype.float({ max: 1 }), score: faker.datatype.float({max: 1}),
}, },
}, },
}, },

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url, options, login = faker.internet.userName() }) { export default function({faker, url, options, login = faker.internet.userName()}) {
//Wakatime api //Wakatime api
if (/^https:..api.poopmap.net.*$/.test(url)) { if (/^https:..api.poopmap.net.*$/.test(url)) {
//Get user profile //Get user profile

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url, options, login = faker.internet.userName() }) { export default function({faker, url, options, login = faker.internet.userName()}) {
//Spotify api //Spotify api
if (/^https:..api.spotify.com.*$/.test(url)) { if (/^https:..api.spotify.com.*$/.test(url)) {
//Get recently played tracks //Get recently played tracks

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url, options, login = faker.internet.userName() }) { export default function({faker, url, options, login = faker.internet.userName()}) {
//Stackoverflow api //Stackoverflow api
if (/^https:..api.stackexchange.com.2.2.*$/.test(url)) { if (/^https:..api.stackexchange.com.2.2.*$/.test(url)) {
//Extract user id //Extract user id
@@ -13,7 +13,7 @@ export default function({ faker, url, options, login = faker.internet.userName()
data: { data: {
items: [ items: [
{ {
badge_counts: { bronze: faker.datatype.number(500), silver: faker.datatype.number(300), gold: faker.datatype.number(100) }, badge_counts: {bronze: faker.datatype.number(500), silver: faker.datatype.number(300), gold: faker.datatype.number(100)},
accept_rate: faker.datatype.number(100), accept_rate: faker.datatype.number(100),
answer_count: faker.datatype.number(1000), answer_count: faker.datatype.number(1000),
question_count: faker.datatype.number(1000), question_count: faker.datatype.number(1000),
@@ -48,7 +48,7 @@ export default function({ faker, url, options, login = faker.internet.userName()
data: { data: {
items: new Array(pagesize).fill(null).map(_ => ({ items: new Array(pagesize).fill(null).map(_ => ({
tags: new Array(5).fill(null).map(_ => faker.lorem.slug()), tags: new Array(5).fill(null).map(_ => faker.lorem.slug()),
owner: { display_name: faker.internet.userName() }, owner: {display_name: faker.internet.userName()},
is_answered: faker.datatype.boolean(), is_answered: faker.datatype.boolean(),
view_count: faker.datatype.number(10000), view_count: faker.datatype.number(10000),
accepted_answer_id: faker.datatype.number(1000000), accepted_answer_id: faker.datatype.number(1000000),
@@ -77,7 +77,7 @@ export default function({ faker, url, options, login = faker.internet.userName()
status: 200, status: 200,
data: { data: {
items: new Array(pagesize).fill(null).map(_ => ({ items: new Array(pagesize).fill(null).map(_ => ({
owner: { display_name: faker.internet.userName() }, owner: {display_name: faker.internet.userName()},
link: faker.internet.url(), link: faker.internet.url(),
is_accepted: faker.datatype.boolean(), is_accepted: faker.datatype.boolean(),
score: faker.datatype.number(1000), score: faker.datatype.number(1000),

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url, options, login = faker.internet.userName() }) { export default function({faker, url, options, login = faker.internet.userName()}) {
//Twitter api //Twitter api
if (/^https:..api.twitter.com.*$/.test(url)) { if (/^https:..api.twitter.com.*$/.test(url)) {
//Get user profile //Get user profile
@@ -31,7 +31,7 @@ export default function({ faker, url, options, login = faker.internet.userName()
created_at: `${faker.date.recent()}`, created_at: `${faker.date.recent()}`,
entities: { entities: {
mentions: [ mentions: [
{ start: 22, end: 33, username: "lowlighter" }, {start: 22, end: 33, username: "lowlighter"},
], ],
}, },
text: "Checkout metrics from @lowlighter ! #GitHub", text: "Checkout metrics from @lowlighter ! #GitHub",

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url, options, login = faker.internet.userName() }) { export default function({faker, url, options, login = faker.internet.userName()}) {
//Wakatime api //Wakatime api
if (/^https:..wakatime.com.api.v1.users..*.stats.*$/.test(url)) { if (/^https:..wakatime.com.api.v1.users..*.stats.*$/.test(url)) {
//Get user profile //Get user profile
@@ -17,7 +17,7 @@ export default function({ faker, url, options, login = faker.internet.userName()
percent: 0, percent: 0,
total_seconds: faker.datatype.number(1000000), total_seconds: faker.datatype.number(1000000),
})) }))
results = results.filter(({ name }) => elements.includes(name) ? false : (elements.push(name), true)) results = results.filter(({name}) => elements.includes(name) ? false : (elements.push(name), true))
let percents = 100 let percents = 100
for (const result of results) { for (const result of results) {
result.percent = 1 + faker.datatype.number(percents - 1) result.percent = 1 + faker.datatype.number(percents - 1)

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url, options, login = faker.internet.userName() }) { export default function({faker, url, options, login = faker.internet.userName()}) {
//Wakatime api //Wakatime api
if (/^https:..apidojo-yahoo-finance-v1.p.rapidapi.com.stock.v2.*$/.test(url)) { if (/^https:..apidojo-yahoo-finance-v1.p.rapidapi.com.stock.v2.*$/.test(url)) {
//Get company profile //Get company profile

View File

@@ -1,10 +1,10 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url, body, login = faker.internet.userName() }) { export default function({faker, url, body, login = faker.internet.userName()}) {
if (/^https:..graphql.anilist.co.*$/.test(url)) { if (/^https:..graphql.anilist.co.*$/.test(url)) {
//Initialization and media generator //Initialization and media generator
const { query } = body const {query} = body
const media = ({ type }) => ({ const media = ({type}) => ({
title: { romaji: faker.lorem.words(), english: faker.lorem.words(), native: faker.lorem.words() }, title: {romaji: faker.lorem.words(), english: faker.lorem.words(), native: faker.lorem.words()},
description: faker.lorem.paragraphs(), description: faker.lorem.paragraphs(),
type, type,
status: faker.random.arrayElement(["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]), status: faker.random.arrayElement(["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]),
@@ -14,8 +14,8 @@ export default function({ faker, url, body, login = faker.internet.userName() })
averageScore: faker.datatype.number(100), averageScore: faker.datatype.number(100),
countryOfOrigin: "JP", countryOfOrigin: "JP",
genres: new Array(6).fill(null).map(_ => faker.lorem.word()), genres: new Array(6).fill(null).map(_ => faker.lorem.word()),
coverImage: { medium: null }, coverImage: {medium: null},
startDate: { year: faker.date.past(20).getFullYear() }, startDate: {year: faker.date.past(20).getFullYear()},
}) })
//User statistics query //User statistics query
if (/^query Statistics /.test(query)) { if (/^query Statistics /.test(query)) {
@@ -33,13 +33,13 @@ export default function({ faker, url, body, login = faker.internet.userName() })
count: faker.datatype.number(1000), count: faker.datatype.number(1000),
minutesWatched: faker.datatype.number(100000), minutesWatched: faker.datatype.number(100000),
episodesWatched: faker.datatype.number(10000), episodesWatched: faker.datatype.number(10000),
genres: new Array(4).fill(null).map(_ => ({ genre: faker.lorem.word() })), genres: new Array(4).fill(null).map(_ => ({genre: faker.lorem.word()})),
}, },
manga: { manga: {
count: faker.datatype.number(1000), count: faker.datatype.number(1000),
chaptersRead: faker.datatype.number(100000), chaptersRead: faker.datatype.number(100000),
volumesRead: faker.datatype.number(10000), volumesRead: faker.datatype.number(10000),
genres: new Array(4).fill(null).map(_ => ({ genre: faker.lorem.word() })), genres: new Array(4).fill(null).map(_ => ({genre: faker.lorem.word()})),
}, },
}, },
}, },
@@ -58,10 +58,10 @@ export default function({ faker, url, body, login = faker.internet.userName() })
favourites: { favourites: {
characters: { characters: {
nodes: new Array(2 + faker.datatype.number(16)).fill(null).map(_ => ({ nodes: new Array(2 + faker.datatype.number(16)).fill(null).map(_ => ({
name: { full: faker.name.findName(), native: faker.name.findName() }, name: {full: faker.name.findName(), native: faker.name.findName()},
image: { medium: null }, image: {medium: null},
})), })),
pageInfo: { currentPage: 1, hasNextPage: false }, pageInfo: {currentPage: 1, hasNextPage: false},
}, },
}, },
}, },
@@ -80,8 +80,8 @@ export default function({ faker, url, body, login = faker.internet.userName() })
User: { User: {
favourites: { favourites: {
[type.toLocaleLowerCase()]: { [type.toLocaleLowerCase()]: {
nodes: new Array(16).fill(null).map(_ => media({ type })), nodes: new Array(16).fill(null).map(_ => media({type})),
pageInfo: { currentPage: 1, hasNextPage: false }, pageInfo: {currentPage: 1, hasNextPage: false},
}, },
}, },
}, },
@@ -92,7 +92,7 @@ export default function({ faker, url, body, login = faker.internet.userName() })
//Medias query //Medias query
if (/^query Medias /.test(query)) { if (/^query Medias /.test(query)) {
console.debug("metrics/compute/mocks > mocking anilist api result > Medias") console.debug("metrics/compute/mocks > mocking anilist api result > Medias")
const { type } = body.variables const {type} = body.variables
return ({ return ({
status: 200, status: 200,
data: { data: {
@@ -100,16 +100,16 @@ export default function({ faker, url, body, login = faker.internet.userName() })
MediaListCollection: { MediaListCollection: {
lists: [ lists: [
{ {
name: { ANIME: "Watching", MANGA: "Reading", OTHER: "Completed" }[type], name: {ANIME: "Watching", MANGA: "Reading", OTHER: "Completed"}[type],
isCustomList: false, isCustomList: false,
entries: new Array(16).fill(null).map(_ => ({ entries: new Array(16).fill(null).map(_ => ({
status: faker.random.arrayElement(["CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"]), status: faker.random.arrayElement(["CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"]),
progress: faker.datatype.number(100), progress: faker.datatype.number(100),
progressVolumes: null, progressVolumes: null,
score: 0, score: 0,
startedAt: { year: null, month: null, day: null }, startedAt: {year: null, month: null, day: null},
completedAt: { year: null, month: null, day: null }, completedAt: {year: null, month: null, day: null},
media: media({ type }), media: media({type}),
})), })),
}, },
], ],

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url, body, login = faker.internet.userName() }) { export default function({faker, url, body, login = faker.internet.userName()}) {
if (/^https:..api.hashnode.com.*$/.test(url)) { if (/^https:..api.hashnode.com.*$/.test(url)) {
console.debug(`metrics/compute/mocks > mocking hashnode result > ${url}`) console.debug(`metrics/compute/mocks > mocking hashnode result > ${url}`)
return ({ return ({

View File

@@ -2,7 +2,7 @@
import urls from "url" import urls from "url"
/**Mocked data */ /**Mocked data */
export default function({ faker, url, body, login = faker.internet.userName() }) { export default function({faker, url, body, login = faker.internet.userName()}) {
if (/^https:..accounts.spotify.com.api.token.*$/.test(url)) { if (/^https:..accounts.spotify.com.api.token.*$/.test(url)) {
//Access token generator //Access token generator
const params = new urls.URLSearchParams(body) const params = new urls.URLSearchParams(body)

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, url, options, login = faker.internet.userName() }) { export default function({faker, url, options, login = faker.internet.userName()}) {
if (/^https:..music.youtube.com.youtubei.v1.*$/.test(url)) { if (/^https:..music.youtube.com.youtubei.v1.*$/.test(url)) {
//Get recently played tracks //Get recently played tracks
if (/browse/.test(url)) { if (/browse/.test(url)) {

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics") console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics")
return ({ return ({
user: { user: {
@@ -22,14 +22,14 @@ export default function({ faker, query, login = faker.internet.userName() }) {
totalCount: faker.datatype.number(100), totalCount: faker.datatype.number(100),
}, },
popular: { popular: {
nodes: [{ stargazers: { totalCount: faker.datatype.number(50000) } }], nodes: [{stargazers: {totalCount: faker.datatype.number(50000)}}],
}, },
pullRequests: { pullRequests: {
nodes: [ nodes: [
{ {
createdAt: faker.date.recent(), createdAt: faker.date.recent(),
title: faker.lorem.sentence(), title: faker.lorem.sentence(),
repository: { nameWithOwner: `${faker.internet.userName()}/${faker.lorem.slug()}` }, repository: {nameWithOwner: `${faker.internet.userName()}/${faker.lorem.slug()}`},
}, },
], ],
totalCount: faker.datatype.number(50000), totalCount: faker.datatype.number(50000),
@@ -42,29 +42,29 @@ export default function({ faker, query, login = faker.internet.userName() }) {
pullRequest: { pullRequest: {
title: faker.lorem.sentence(), title: faker.lorem.sentence(),
number: faker.datatype.number(1000), number: faker.datatype.number(1000),
repository: { nameWithOwner: `${faker.internet.userName()}/${faker.lorem.slug()}` }, repository: {nameWithOwner: `${faker.internet.userName()}/${faker.lorem.slug()}`},
}, },
}, },
], ],
totalCount: faker.datatype.number(1000), totalCount: faker.datatype.number(1000),
}, },
}, },
projects: { totalCount: faker.datatype.number(100) }, projects: {totalCount: faker.datatype.number(100)},
packages: { totalCount: faker.datatype.number(100) }, packages: {totalCount: faker.datatype.number(100)},
organizations: { nodes: [], totalCount: faker.datatype.number(5) }, organizations: {nodes: [], totalCount: faker.datatype.number(5)},
gists: { gists: {
nodes: [{ createdAt: faker.date.recent(), name: faker.lorem.slug() }], nodes: [{createdAt: faker.date.recent(), name: faker.lorem.slug()}],
totalCount: faker.datatype.number(1000), totalCount: faker.datatype.number(1000),
}, },
starredRepositories: { totalCount: faker.datatype.number(1000) }, starredRepositories: {totalCount: faker.datatype.number(1000)},
followers: { totalCount: faker.datatype.number(10000) }, followers: {totalCount: faker.datatype.number(10000)},
following: { totalCount: faker.datatype.number(10000) }, following: {totalCount: faker.datatype.number(10000)},
bio: faker.lorem.sentence(), bio: faker.lorem.sentence(),
status: { message: faker.lorem.paragraph() }, status: {message: faker.lorem.paragraph()},
sponsorshipsAsSponsor: { totalCount: faker.datatype.number(100) }, sponsorshipsAsSponsor: {totalCount: faker.datatype.number(100)},
discussionsStarted: { totalCount: faker.datatype.number(1000) }, discussionsStarted: {totalCount: faker.datatype.number(1000)},
discussionsComments: { totalCount: faker.datatype.number(1000) }, discussionsComments: {totalCount: faker.datatype.number(1000)},
discussionAnswers: { totalCount: faker.datatype.number(1000) }, discussionAnswers: {totalCount: faker.datatype.number(1000)},
}, },
}) })
} }

View File

@@ -1,8 +1,8 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics") console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics")
return ({ return ({
repository: { viewerHasStarred: faker.datatype.boolean() }, repository: {viewerHasStarred: faker.datatype.boolean()},
viewer: { login }, viewer: {login},
}) })
} }

View File

@@ -1,8 +1,8 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/octocat") console.debug("metrics/compute/mocks > mocking graphql api result > achievements/octocat")
return ({ return ({
user: { viewerIsFollowing: faker.datatype.boolean() }, user: {viewerIsFollowing: faker.datatype.boolean()},
viewer: { login }, viewer: {login},
}) })
} }

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/organizations") console.debug("metrics/compute/mocks > mocking graphql api result > achievements/organizations")
return ({ return ({
organization: { organization: {
@@ -22,12 +22,12 @@ export default function({ faker, query, login = faker.internet.userName() }) {
totalCount: faker.datatype.number(100), totalCount: faker.datatype.number(100),
}, },
popular: { popular: {
nodes: [{ stargazers: { totalCount: faker.datatype.number(50000) } }], nodes: [{stargazers: {totalCount: faker.datatype.number(50000)}}],
}, },
projects: { totalCount: faker.datatype.number(100) }, projects: {totalCount: faker.datatype.number(100)},
packages: { totalCount: faker.datatype.number(100) }, packages: {totalCount: faker.datatype.number(100)},
membersWithRole: { totalCount: faker.datatype.number(100) }, membersWithRole: {totalCount: faker.datatype.number(100)},
sponsorshipsAsSponsor: { totalCount: faker.datatype.number(100) }, sponsorshipsAsSponsor: {totalCount: faker.datatype.number(100)},
}, },
}) })
} }

View File

@@ -1,12 +1,12 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/ranking") console.debug("metrics/compute/mocks > mocking graphql api result > achievements/ranking")
return ({ return ({
repo_rank: { repositoryCount: faker.datatype.number(100000) }, repo_rank: {repositoryCount: faker.datatype.number(100000)},
forks_rank: { repositoryCount: faker.datatype.number(100000) }, forks_rank: {repositoryCount: faker.datatype.number(100000)},
created_rank: { userCount: faker.datatype.number(100000) }, created_rank: {userCount: faker.datatype.number(100000)},
user_rank: { userCount: faker.datatype.number(100000) }, user_rank: {userCount: faker.datatype.number(100000)},
repo_total: { repositoryCount: faker.datatype.number(100000) }, repo_total: {repositoryCount: faker.datatype.number(100000)},
user_total: { userCount: faker.datatype.number(100000) }, user_total: {userCount: faker.datatype.number(100000)},
}) })
} }

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/user") console.debug("metrics/compute/mocks > mocking graphql api result > base/user")
return ({ return ({
user: { user: {
@@ -8,29 +8,29 @@ export default function({ faker, query, login = faker.internet.userName() }) {
weeks: [ weeks: [
{ {
contributionDays: [ contributionDays: [
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
], ],
}, },
{ {
contributionDays: [ contributionDays: [
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
], ],
}, },
{ {
contributionDays: [ contributionDays: [
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
{ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) }, {color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])},
], ],
}, },
], ],

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/user") console.debug("metrics/compute/mocks > mocking graphql api result > base/user")
return ({ return ({
user: { user: {

View File

@@ -1,18 +1,18 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/user") console.debug("metrics/compute/mocks > mocking graphql api result > base/user")
return ({ return ({
user: { user: {
packages: { totalCount: faker.datatype.number(10) }, packages: {totalCount: faker.datatype.number(10)},
starredRepositories: { totalCount: faker.datatype.number(1000) }, starredRepositories: {totalCount: faker.datatype.number(1000)},
watching: { totalCount: faker.datatype.number(100) }, watching: {totalCount: faker.datatype.number(100)},
sponsorshipsAsSponsor: { totalCount: faker.datatype.number(10) }, sponsorshipsAsSponsor: {totalCount: faker.datatype.number(10)},
sponsorshipsAsMaintainer: { totalCount: faker.datatype.number(10) }, sponsorshipsAsMaintainer: {totalCount: faker.datatype.number(10)},
repositoriesContributedTo: { totalCount: faker.datatype.number(100) }, repositoriesContributedTo: {totalCount: faker.datatype.number(100)},
followers: { totalCount: faker.datatype.number(1000) }, followers: {totalCount: faker.datatype.number(1000)},
following: { totalCount: faker.datatype.number(1000) }, following: {totalCount: faker.datatype.number(1000)},
issueComments: { totalCount: faker.datatype.number(1000) }, issueComments: {totalCount: faker.datatype.number(1000)},
organizations: { totalCount: faker.datatype.number(10) }, organizations: {totalCount: faker.datatype.number(10)},
}, },
}) })
} }

View File

@@ -1,9 +1,9 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/user") console.debug("metrics/compute/mocks > mocking graphql api result > base/user")
return ({ return ({
user: { user: {
repositories: { totalCount: faker.datatype.number(100), totalDiskUsage: faker.datatype.number(100000) }, repositories: {totalCount: faker.datatype.number(100), totalDiskUsage: faker.datatype.number(100000)},
}, },
}) })
} }

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/repositories") console.debug("metrics/compute/mocks > mocking graphql api result > base/repositories")
return /after: "MOCKED_CURSOR"/m.test(query) return /after: "MOCKED_CURSOR"/m.test(query)
? ({ ? ({
@@ -27,30 +27,30 @@ export default function({ faker, query, login = faker.internet.userName() }) {
nodes: [ nodes: [
{ {
name: faker.random.words(), name: faker.random.words(),
watchers: { totalCount: faker.datatype.number(1000) }, watchers: {totalCount: faker.datatype.number(1000)},
stargazers: { totalCount: faker.datatype.number(10000) }, stargazers: {totalCount: faker.datatype.number(10000)},
owner: { login }, owner: {login},
languages: { languages: {
edges: [ edges: [
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
], ],
}, },
issues_open: { totalCount: faker.datatype.number(100) }, issues_open: {totalCount: faker.datatype.number(100)},
issues_closed: { totalCount: faker.datatype.number(100) }, issues_closed: {totalCount: faker.datatype.number(100)},
pr_open: { totalCount: faker.datatype.number(100) }, pr_open: {totalCount: faker.datatype.number(100)},
pr_closed: { totalCount: faker.datatype.number(100) }, pr_closed: {totalCount: faker.datatype.number(100)},
pr_merged: { totalCount: faker.datatype.number(100) }, pr_merged: {totalCount: faker.datatype.number(100)},
releases: { totalCount: faker.datatype.number(100) }, releases: {totalCount: faker.datatype.number(100)},
forkCount: faker.datatype.number(100), forkCount: faker.datatype.number(100),
licenseInfo: { spdxId: "MIT" }, licenseInfo: {spdxId: "MIT"},
deployments: { totalCount: faker.datatype.number(100) }, deployments: {totalCount: faker.datatype.number(100)},
environments: { totalCount: faker.datatype.number(100) }, environments: {totalCount: faker.datatype.number(100)},
}, },
], ],
}, },

View File

@@ -1,37 +1,37 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/repository") console.debug("metrics/compute/mocks > mocking graphql api result > base/repository")
return ({ return ({
user: { user: {
repository: { repository: {
name: "metrics", name: "metrics",
owner: { login }, owner: {login},
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
diskUsage: Math.floor(Math.random() * 10000), diskUsage: Math.floor(Math.random() * 10000),
homepageUrl: faker.internet.url(), homepageUrl: faker.internet.url(),
watchers: { totalCount: faker.datatype.number(1000) }, watchers: {totalCount: faker.datatype.number(1000)},
stargazers: { totalCount: faker.datatype.number(10000) }, stargazers: {totalCount: faker.datatype.number(10000)},
languages: { languages: {
edges: [ edges: [
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
{ size: faker.datatype.number(100000), node: { color: faker.internet.color(), name: faker.lorem.word() } }, {size: faker.datatype.number(100000), node: {color: faker.internet.color(), name: faker.lorem.word()}},
], ],
}, },
issues_open: { totalCount: faker.datatype.number(100) }, issues_open: {totalCount: faker.datatype.number(100)},
issues_closed: { totalCount: faker.datatype.number(100) }, issues_closed: {totalCount: faker.datatype.number(100)},
pr_open: { totalCount: faker.datatype.number(100) }, pr_open: {totalCount: faker.datatype.number(100)},
pr_closed: { totalCount: faker.datatype.number(100) }, pr_closed: {totalCount: faker.datatype.number(100)},
pr_merged: { totalCount: faker.datatype.number(100) }, pr_merged: {totalCount: faker.datatype.number(100)},
releases: { totalCount: faker.datatype.number(100) }, releases: {totalCount: faker.datatype.number(100)},
forkCount: faker.datatype.number(100), forkCount: faker.datatype.number(100),
licenseInfo: { spdxId: "MIT" }, licenseInfo: {spdxId: "MIT"},
deployments: { totalCount: faker.datatype.number(100) }, deployments: {totalCount: faker.datatype.number(100)},
environments: { totalCount: faker.datatype.number(100) }, environments: {totalCount: faker.datatype.number(100)},
}, },
}, },
}) })

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > base/user") console.debug("metrics/compute/mocks > mocking graphql api result > base/user")
return ({ return ({
user: { user: {

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > contributors/commit") console.debug("metrics/compute/mocks > mocking graphql api result > contributors/commit")
return ({ return ({
repository: { repository: {

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > discussions/categories") console.debug("metrics/compute/mocks > mocking graphql api result > discussions/categories")
return /after: "MOCKED_CURSOR"/m.test(query) return /after: "MOCKED_CURSOR"/m.test(query)
? ({ ? ({
@@ -13,7 +13,7 @@ export default function({ faker, query, login = faker.internet.userName() }) {
: ({ : ({
user: { user: {
repositoryDiscussions: { repositoryDiscussions: {
edges: new Array(100).fill(null).map(_ => ({ cursor: "MOCKED_CURSOR" })), edges: new Array(100).fill(null).map(_ => ({cursor: "MOCKED_CURSOR"})),
nodes: new Array(100).fill(null).map(_ => ({ nodes: new Array(100).fill(null).map(_ => ({
category: { category: {
emoji: faker.random.arrayElement([":chart_with_upwards_trend:", ":chart_with_downwards_trend:", ":bar_char:"]), emoji: faker.random.arrayElement([":chart_with_upwards_trend:", ":chart_with_downwards_trend:", ":bar_char:"]),

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > discussions/comments") console.debug("metrics/compute/mocks > mocking graphql api result > discussions/comments")
return /after: "MOCKED_CURSOR"/m.test(query) return /after: "MOCKED_CURSOR"/m.test(query)
? ({ ? ({
@@ -13,8 +13,8 @@ export default function({ faker, query, login = faker.internet.userName() }) {
: ({ : ({
user: { user: {
repositoryDiscussionsComments: { repositoryDiscussionsComments: {
edges: new Array(100).fill(null).map(_ => ({ cursor: "MOCKED_CURSOR" })), edges: new Array(100).fill(null).map(_ => ({cursor: "MOCKED_CURSOR"})),
nodes: new Array(100).fill(null).map(_ => ({ upvoteCount: faker.datatype.number(10) })), nodes: new Array(100).fill(null).map(_ => ({upvoteCount: faker.datatype.number(10)})),
}, },
}, },
}) })

View File

@@ -1,11 +1,11 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > discussions/statistics") console.debug("metrics/compute/mocks > mocking graphql api result > discussions/statistics")
return ({ return ({
user: { user: {
started: { totalCount: faker.datatype.number(1000) }, started: {totalCount: faker.datatype.number(1000)},
comments: { totalCount: faker.datatype.number(1000) }, comments: {totalCount: faker.datatype.number(1000)},
answers: { totalCount: faker.datatype.number(1000) }, answers: {totalCount: faker.datatype.number(1000)},
}, },
}) })
} }

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > followup/repository/collaborators") console.debug("metrics/compute/mocks > mocking graphql api result > followup/repository/collaborators")
return ({ return ({
repository: { repository: {

View File

@@ -1,14 +1,14 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > followup/repository") console.debug("metrics/compute/mocks > mocking graphql api result > followup/repository")
return ({ return ({
issues_open: { issueCount: faker.datatype.number(100) }, issues_open: {issueCount: faker.datatype.number(100)},
issues_drafts: { issueCount: faker.datatype.number(100) }, issues_drafts: {issueCount: faker.datatype.number(100)},
issues_skipped: { issueCount: faker.datatype.number(100) }, issues_skipped: {issueCount: faker.datatype.number(100)},
issues_closed: { issueCount: faker.datatype.number(100) }, issues_closed: {issueCount: faker.datatype.number(100)},
pr_open: { issueCount: faker.datatype.number(100) }, pr_open: {issueCount: faker.datatype.number(100)},
pr_drafts: { issueCount: faker.datatype.number(100) }, pr_drafts: {issueCount: faker.datatype.number(100)},
pr_closed: { issueCount: faker.datatype.number(100) }, pr_closed: {issueCount: faker.datatype.number(100)},
pr_merged: { issueCount: faker.datatype.number(100) }, pr_merged: {issueCount: faker.datatype.number(100)},
}) })
} }

View File

@@ -1,14 +1,14 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > followup/user") console.debug("metrics/compute/mocks > mocking graphql api result > followup/user")
return ({ return ({
issues_open: { issueCount: faker.datatype.number(100) }, issues_open: {issueCount: faker.datatype.number(100)},
issues_drafts: { issueCount: faker.datatype.number(100) }, issues_drafts: {issueCount: faker.datatype.number(100)},
issues_skipped: { issueCount: faker.datatype.number(100) }, issues_skipped: {issueCount: faker.datatype.number(100)},
issues_closed: { issueCount: faker.datatype.number(100) }, issues_closed: {issueCount: faker.datatype.number(100)},
pr_open: { issueCount: faker.datatype.number(100) }, pr_open: {issueCount: faker.datatype.number(100)},
pr_drafts: { issueCount: faker.datatype.number(100) }, pr_drafts: {issueCount: faker.datatype.number(100)},
pr_closed: { issueCount: faker.datatype.number(100) }, pr_closed: {issueCount: faker.datatype.number(100)},
pr_merged: { issueCount: faker.datatype.number(100) }, pr_merged: {issueCount: faker.datatype.number(100)},
}) })
} }

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > gists/default") console.debug("metrics/compute/mocks > mocking graphql api result > gists/default")
return /after: "MOCKED_CURSOR"/m.test(query) return /after: "MOCKED_CURSOR"/m.test(query)
? ({ ? ({
@@ -23,16 +23,16 @@ export default function({ faker, query, login = faker.internet.userName() }) {
{ {
stargazerCount: faker.datatype.number(10), stargazerCount: faker.datatype.number(10),
isFork: false, isFork: false,
forks: { totalCount: faker.datatype.number(10) }, forks: {totalCount: faker.datatype.number(10)},
files: [{ name: faker.system.fileName() }], files: [{name: faker.system.fileName()}],
comments: { totalCount: faker.datatype.number(10) }, comments: {totalCount: faker.datatype.number(10)},
}, },
{ {
stargazerCount: faker.datatype.number(10), stargazerCount: faker.datatype.number(10),
isFork: false, isFork: false,
forks: { totalCount: faker.datatype.number(10) }, forks: {totalCount: faker.datatype.number(10)},
files: [{ name: faker.system.fileName() }], files: [{name: faker.system.fileName()}],
comments: { totalCount: faker.datatype.number(10) }, comments: {totalCount: faker.datatype.number(10)},
}, },
], ],
}, },

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/organization") console.debug("metrics/compute/mocks > mocking graphql api result > introduction/organization")
return ({ return ({
organization: { organization: {

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/repository") console.debug("metrics/compute/mocks > mocking graphql api result > introduction/repository")
return ({ return ({
repository: { repository: {

View File

@@ -1,5 +1,5 @@
/**Mocked data */ /**Mocked data */
export default function({ faker, query, login = faker.internet.userName() }) { export default function({faker, query, login = faker.internet.userName()}) {
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/user") console.debug("metrics/compute/mocks > mocking graphql api result > introduction/user")
return ({ return ({
user: { user: {

Some files were not shown because too many files have changed in this diff Show More