Code formatting (#280)
This commit is contained in:
@@ -1,433 +1,468 @@
|
||||
//Imports
|
||||
import core from "@actions/core"
|
||||
import github from "@actions/github"
|
||||
import octokit from "@octokit/graphql"
|
||||
import setup from "../metrics/setup.mjs"
|
||||
import mocks from "../mocks/index.mjs"
|
||||
import metrics from "../metrics/index.mjs"
|
||||
import fs from "fs/promises"
|
||||
import paths from "path"
|
||||
import sgit from "simple-git"
|
||||
process.on("unhandledRejection", error => {
|
||||
throw error
|
||||
})
|
||||
import core from "@actions/core"
|
||||
import github from "@actions/github"
|
||||
import octokit from "@octokit/graphql"
|
||||
import fs from "fs/promises"
|
||||
import paths from "path"
|
||||
import sgit from "simple-git"
|
||||
import metrics from "../metrics/index.mjs"
|
||||
import setup from "../metrics/setup.mjs"
|
||||
import mocks from "../mocks/index.mjs"
|
||||
process.on("unhandledRejection", error => {
|
||||
throw error
|
||||
})
|
||||
|
||||
//Debug message buffer
|
||||
let DEBUG = true
|
||||
const debugged = []
|
||||
let DEBUG = true
|
||||
const debugged = []
|
||||
|
||||
//Info logger
|
||||
const info = (left, right, {token = false} = {}) => console.log(`${`${left}`.padEnd(56 + 9*(/0m$/.test(left)))} │ ${
|
||||
Array.isArray(right) ? right.join(", ") || "(none)" :
|
||||
right === undefined ? "(default)" :
|
||||
token ? /^MOCKED/.test(right) ? "(MOCKED TOKEN)" : /^NOT_NEEDED$/.test(right) ? "(NOT NEEDED)" : (right ? "(provided)" : "(missing)") :
|
||||
typeof right === "object" ? JSON.stringify(right) :
|
||||
right
|
||||
const info = (left, right, {token = false} = {}) => console.log(`${`${left}`.padEnd(56 + 9 * (/0m$/.test(left)))} │ ${
|
||||
Array.isArray(right)
|
||||
? right.join(", ") || "(none)"
|
||||
: right === undefined
|
||||
? "(default)"
|
||||
: token
|
||||
? /^MOCKED/.test(right) ? "(MOCKED TOKEN)" : /^NOT_NEEDED$/.test(right) ? "(NOT NEEDED)" : (right ? "(provided)" : "(missing)")
|
||||
: typeof right === "object"
|
||||
? JSON.stringify(right)
|
||||
: right
|
||||
}`)
|
||||
info.section = (left = "", right = " ") => info(`\x1b[36m${left}\x1b[0m`, right)
|
||||
info.group = ({metadata, name, inputs}) => {
|
||||
info.section(metadata.plugins[name]?.name?.match(/(?<section>[\w\s]+)/i)?.groups?.section?.trim(), " ")
|
||||
for (const [input, value] of Object.entries(inputs))
|
||||
info(metadata.plugins[name]?.inputs[input]?.description ?? input, value, {token:metadata.plugins[name]?.inputs[input]?.type === "token"})
|
||||
}
|
||||
info.break = () => console.log("─".repeat(88))
|
||||
info.section = (left = "", right = " ") => info(`\x1b[36m${left}\x1b[0m`, right)
|
||||
info.group = ({metadata, name, inputs}) => {
|
||||
info.section(metadata.plugins[name]?.name?.match(/(?<section>[\w\s]+)/i)?.groups?.section?.trim(), " ")
|
||||
for (const [input, value] of Object.entries(inputs))
|
||||
info(metadata.plugins[name]?.inputs[input]?.description ?? input, value, {token:metadata.plugins[name]?.inputs[input]?.type === "token"})
|
||||
}
|
||||
info.break = () => console.log("─".repeat(88))
|
||||
|
||||
//Waiter
|
||||
async function wait(seconds) {
|
||||
await new Promise(solve => setTimeout(solve, seconds*1000))
|
||||
}
|
||||
async function wait(seconds) {
|
||||
await new Promise(solve => setTimeout(solve, seconds * 1000))
|
||||
}
|
||||
|
||||
//Runner
|
||||
(async function() {
|
||||
(async function() {
|
||||
try {
|
||||
//Initialization
|
||||
info.break()
|
||||
info.section("Metrics")
|
||||
|
||||
//Skip process if needed
|
||||
if ((github.context.eventName === "push") && (github.context.payload?.head_commit)) {
|
||||
if (/\[Skip GitHub Action\]/.test(github.context.payload.head_commit.message)) {
|
||||
console.log("Skipped because [Skip GitHub Action] is in commit message")
|
||||
process.exit(0)
|
||||
}
|
||||
if (/Auto-generated metrics for run #\d+/.test(github.context.payload.head_commit.message)) {
|
||||
console.log("Skipped because this seems to be an automated pull request merge")
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
//Load configuration
|
||||
const {conf, Plugins, Templates} = await setup({log:false, nosettings:true, community:{templates:core.getInput("setup_community_templates")}})
|
||||
const {metadata} = conf
|
||||
conf.settings.extras = {default:true}
|
||||
info("Setup", "complete")
|
||||
info("Version", conf.package.version)
|
||||
|
||||
//Core inputs
|
||||
const {
|
||||
user:_user,
|
||||
repo:_repo,
|
||||
token,
|
||||
template,
|
||||
query,
|
||||
"setup.community.templates":_templates,
|
||||
filename:_filename,
|
||||
optimize,
|
||||
verify,
|
||||
"markdown.cache":_markdown_cache,
|
||||
debug,
|
||||
"debug.flags":dflags,
|
||||
"use.mocked.data":mocked,
|
||||
dryrun,
|
||||
"plugins.errors.fatal":die,
|
||||
"committer.token":_token,
|
||||
"committer.branch":_branch,
|
||||
"committer.message":_message,
|
||||
"committer.gist":_gist,
|
||||
"use.prebuilt.image":_image,
|
||||
retries,
|
||||
"retries.delay":retries_delay,
|
||||
"output.action":_action,
|
||||
...config
|
||||
} = metadata.plugins.core.inputs.action({core})
|
||||
const q = {...query, ...(_repo ? {repo:_repo} : null), template}
|
||||
const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].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"}[_output] ?? _output)
|
||||
|
||||
//Docker image
|
||||
if (_image)
|
||||
info("Using prebuilt image", _image)
|
||||
|
||||
//Debug mode and flags
|
||||
info("Debug mode", debug)
|
||||
if (!debug) {
|
||||
console.debug = message => debugged.push(message)
|
||||
DEBUG = false
|
||||
}
|
||||
info("Debug flags", dflags)
|
||||
q["debug.flags"] = dflags.join(" ")
|
||||
|
||||
//Token for data gathering
|
||||
info("GitHub token", token, {token:true})
|
||||
if (!token)
|
||||
throw new Error("You must provide a valid GitHub personal token to gather your metrics (see https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/setup/action/setup.md for more informations)")
|
||||
conf.settings.token = token
|
||||
const api = {}
|
||||
api.graphql = octokit.graphql.defaults({headers:{authorization:`token ${token}`}})
|
||||
info("Github GraphQL API", "ok")
|
||||
api.rest = github.getOctokit(token)
|
||||
info("Github REST API", "ok")
|
||||
//Apply mocking if needed
|
||||
if (mocked) {
|
||||
Object.assign(api, await mocks(api))
|
||||
info("Use mocked API", true)
|
||||
}
|
||||
//Test token validity
|
||||
else if (!/^NOT_NEEDED$/.test(token)) {
|
||||
const {headers} = await api.rest.request("HEAD /")
|
||||
if (!("x-oauth-scopes" in headers)) {
|
||||
throw new Error(
|
||||
'GitHub API did not send any "x-oauth-scopes" header back from provided "token". It means that your token may not be valid or you\'re using GITHUB_TOKEN which cannot be used since metrics will fetch data outside of this repository scope. Use a personal access token instead (see https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/setup/action/setup.md for more informations).',
|
||||
)
|
||||
}
|
||||
info("Token validity", "seems ok")
|
||||
}
|
||||
//Extract octokits
|
||||
const {graphql, rest} = api
|
||||
|
||||
//GitHub user
|
||||
let authenticated
|
||||
try {
|
||||
authenticated = (await rest.users.getAuthenticated()).data.login
|
||||
}
|
||||
catch {
|
||||
authenticated = github.context.repo.owner
|
||||
}
|
||||
const user = _user || authenticated
|
||||
conf.authenticated = user
|
||||
info("GitHub account", user)
|
||||
if (q.repo)
|
||||
info("GitHub repository", `${user}/${q.repo}`)
|
||||
|
||||
//Current repository
|
||||
info("Current repository", `${github.context.repo.owner}/${github.context.repo.repo}`)
|
||||
|
||||
//Committer
|
||||
const committer = {}
|
||||
if (!dryrun) {
|
||||
//Compute committer informations
|
||||
committer.token = _token || token
|
||||
committer.gist = _action === "gist" ? _gist : null
|
||||
committer.commit = true
|
||||
committer.message = _message.replace(/[$][{]filename[}]/g, filename)
|
||||
committer.pr = /^pull-request/.test(_action)
|
||||
committer.merge = _action.match(/^pull-request-(?<method>merge|squash|rebase)$/)?.groups?.method ?? null
|
||||
committer.branch = _branch || github.context.ref.replace(/^refs[/]heads[/]/, "")
|
||||
committer.head = committer.pr ? `metrics-run-${github.context.runId}` : committer.branch
|
||||
info("Committer token", committer.token, {token:true})
|
||||
if (!committer.token)
|
||||
throw new Error("You must provide a valid GitHub token to commit your metrics")
|
||||
info("Committer branch", committer.branch)
|
||||
info("Committer head branch", committer.head)
|
||||
//Gist
|
||||
if (committer.gist)
|
||||
info("Committer Gist id", committer.gist)
|
||||
//Instantiate API for committer
|
||||
committer.rest = github.getOctokit(committer.token)
|
||||
info("Committer REST API", "ok")
|
||||
try {
|
||||
//Initialization
|
||||
info.break()
|
||||
info.section("Metrics")
|
||||
info("Committer account", (await committer.rest.users.getAuthenticated()).data.login)
|
||||
}
|
||||
catch {
|
||||
info("Committer account", "(github-actions)")
|
||||
}
|
||||
//Create head branch if needed
|
||||
try {
|
||||
await committer.rest.git.getRef({...github.context.repo, ref:`heads/${committer.head}`})
|
||||
info("Committer head branch status", "ok")
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
if (/not found/i.test(`${error}`)) {
|
||||
const {data:{object:{sha}}} = await committer.rest.git.getRef({...github.context.repo, ref:`heads/${committer.branch}`})
|
||||
info("Committer branch current sha", sha)
|
||||
await committer.rest.git.createRef({...github.context.repo, ref:`refs/heads/${committer.head}`, sha})
|
||||
info("Committer head branch status", "(created)")
|
||||
}
|
||||
else
|
||||
throw error
|
||||
|
||||
//Skip process if needed
|
||||
if ((github.context.eventName === "push")&&(github.context.payload?.head_commit)) {
|
||||
if (/\[Skip GitHub Action\]/.test(github.context.payload.head_commit.message)) {
|
||||
console.log("Skipped because [Skip GitHub Action] is in commit message")
|
||||
process.exit(0)
|
||||
}
|
||||
if (/Auto-generated metrics for run #\d+/.test(github.context.payload.head_commit.message)) {
|
||||
console.log("Skipped because this seems to be an automated pull request merge")
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
//Load configuration
|
||||
const {conf, Plugins, Templates} = await setup({log:false, nosettings:true, community:{templates:core.getInput("setup_community_templates")}})
|
||||
const {metadata} = conf
|
||||
conf.settings.extras = {default:true}
|
||||
info("Setup", "complete")
|
||||
info("Version", conf.package.version)
|
||||
|
||||
//Core inputs
|
||||
const {
|
||||
user:_user, repo:_repo, token,
|
||||
template, query, "setup.community.templates":_templates,
|
||||
filename:_filename, optimize, verify, "markdown.cache":_markdown_cache,
|
||||
debug, "debug.flags":dflags, "use.mocked.data":mocked, dryrun,
|
||||
"plugins.errors.fatal":die,
|
||||
"committer.token":_token, "committer.branch":_branch, "committer.message":_message, "committer.gist":_gist,
|
||||
"use.prebuilt.image":_image,
|
||||
retries, "retries.delay":retries_delay,
|
||||
"output.action":_action,
|
||||
...config
|
||||
} = metadata.plugins.core.inputs.action({core})
|
||||
const q = {...query, ...(_repo ? {repo:_repo} : null), template}
|
||||
const _output = ["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].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"}[_output] ?? _output)
|
||||
|
||||
//Docker image
|
||||
if (_image)
|
||||
info("Using prebuilt image", _image)
|
||||
|
||||
//Debug mode and flags
|
||||
info("Debug mode", debug)
|
||||
if (!debug) {
|
||||
console.debug = message => debugged.push(message)
|
||||
DEBUG = false
|
||||
}
|
||||
info("Debug flags", dflags)
|
||||
q["debug.flags"] = dflags.join(" ")
|
||||
|
||||
//Token for data gathering
|
||||
info("GitHub token", token, {token:true})
|
||||
if (!token)
|
||||
throw new Error("You must provide a valid GitHub personal token to gather your metrics (see https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/setup/action/setup.md for more informations)")
|
||||
conf.settings.token = token
|
||||
const api = {}
|
||||
api.graphql = octokit.graphql.defaults({headers:{authorization:`token ${token}`}})
|
||||
info("Github GraphQL API", "ok")
|
||||
api.rest = github.getOctokit(token)
|
||||
info("Github REST API", "ok")
|
||||
//Apply mocking if needed
|
||||
if (mocked) {
|
||||
Object.assign(api, await mocks(api))
|
||||
info("Use mocked API", true)
|
||||
}
|
||||
//Test token validity
|
||||
else if (!/^NOT_NEEDED$/.test(token)) {
|
||||
const {headers} = await api.rest.request("HEAD /")
|
||||
if (!("x-oauth-scopes" in headers))
|
||||
throw new Error("GitHub API did not send any \"x-oauth-scopes\" header back from provided \"token\". It means that your token may not be valid or you're using GITHUB_TOKEN which cannot be used since metrics will fetch data outside of this repository scope. Use a personal access token instead (see https://github.com/lowlighter/metrics/blob/master/.github/readme/partials/setup/action/setup.md for more informations).")
|
||||
info("Token validity", "seems ok")
|
||||
}
|
||||
//Extract octokits
|
||||
const {graphql, rest} = api
|
||||
|
||||
//GitHub user
|
||||
let authenticated
|
||||
try {
|
||||
authenticated = (await rest.users.getAuthenticated()).data.login
|
||||
}
|
||||
catch {
|
||||
authenticated = github.context.repo.owner
|
||||
}
|
||||
const user = _user || authenticated
|
||||
conf.authenticated = user
|
||||
info("GitHub account", user)
|
||||
if (q.repo)
|
||||
info("GitHub repository", `${user}/${q.repo}`)
|
||||
|
||||
//Current repository
|
||||
info("Current repository", `${github.context.repo.owner}/${github.context.repo.repo}`)
|
||||
|
||||
//Committer
|
||||
const committer = {}
|
||||
if (!dryrun) {
|
||||
//Compute committer informations
|
||||
committer.token = _token || token
|
||||
committer.gist = _action === "gist" ? _gist : null
|
||||
committer.commit = true
|
||||
committer.message = _message.replace(/[$][{]filename[}]/g, filename)
|
||||
committer.pr = /^pull-request/.test(_action)
|
||||
committer.merge = _action.match(/^pull-request-(?<method>merge|squash|rebase)$/)?.groups?.method ?? null
|
||||
committer.branch = _branch || github.context.ref.replace(/^refs[/]heads[/]/, "")
|
||||
committer.head = committer.pr ? `metrics-run-${github.context.runId}` : committer.branch
|
||||
info("Committer token", committer.token, {token:true})
|
||||
if (!committer.token)
|
||||
throw new Error("You must provide a valid GitHub token to commit your metrics")
|
||||
info("Committer branch", committer.branch)
|
||||
info("Committer head branch", committer.head)
|
||||
//Gist
|
||||
if (committer.gist)
|
||||
info("Committer Gist id", committer.gist)
|
||||
//Instantiate API for committer
|
||||
committer.rest = github.getOctokit(committer.token)
|
||||
info("Committer REST API", "ok")
|
||||
try {
|
||||
info("Committer account", (await committer.rest.users.getAuthenticated()).data.login)
|
||||
}
|
||||
catch {
|
||||
info("Committer account", "(github-actions)")
|
||||
}
|
||||
//Create head branch if needed
|
||||
try {
|
||||
await committer.rest.git.getRef({...github.context.repo, ref:`heads/${committer.head}`})
|
||||
info("Committer head branch status", "ok")
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
if (/not found/i.test(`${error}`)) {
|
||||
const {data:{object:{sha}}} = await committer.rest.git.getRef({...github.context.repo, ref:`heads/${committer.branch}`})
|
||||
info("Committer branch current sha", sha)
|
||||
await committer.rest.git.createRef({...github.context.repo, ref:`refs/heads/${committer.head}`, sha})
|
||||
info("Committer head branch status", "(created)")
|
||||
}
|
||||
else
|
||||
throw error
|
||||
}
|
||||
//Retrieve previous render SHA to be able to update file content through API
|
||||
committer.sha = null
|
||||
try {
|
||||
const {repository:{object:{oid}}} = await graphql(`
|
||||
}
|
||||
//Retrieve previous render SHA to be able to update file content through API
|
||||
committer.sha = null
|
||||
try {
|
||||
const {repository:{object:{oid}}} = await graphql(
|
||||
`
|
||||
query Sha {
|
||||
repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
|
||||
object(expression: "${committer.head}:${filename}") { ... on Blob { oid } }
|
||||
}
|
||||
}
|
||||
`, {headers:{authorization:`token ${committer.token}`}})
|
||||
committer.sha = oid
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
}
|
||||
info("Previous render sha", committer.sha ?? "(none)")
|
||||
}
|
||||
`,
|
||||
{headers:{authorization:`token ${committer.token}`}},
|
||||
)
|
||||
committer.sha = oid
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
}
|
||||
info("Previous render sha", committer.sha ?? "(none)")
|
||||
}
|
||||
else
|
||||
info("Dry-run", true)
|
||||
|
||||
|
||||
//SVG file
|
||||
conf.settings.optimize = optimize
|
||||
info("SVG output", filename)
|
||||
info("SVG optimization", optimize)
|
||||
info("SVG verification after generation", verify)
|
||||
|
||||
//Template
|
||||
info.break()
|
||||
info.section("Templates")
|
||||
info("Community templates", _templates)
|
||||
info("Template used", template)
|
||||
info("Query additional params", query)
|
||||
|
||||
//Core config
|
||||
info.break()
|
||||
info.group({metadata, name:"core", inputs:config})
|
||||
info("Plugin errors", die ? "(exit with error)" : "(displayed in generated image)")
|
||||
const convert = _output || null
|
||||
Object.assign(q, config)
|
||||
if (/markdown/.test(convert))
|
||||
info("Markdown cache", _markdown_cache)
|
||||
|
||||
//Base content
|
||||
info.break()
|
||||
const {base:parts, ...base} = metadata.plugins.base.inputs.action({core})
|
||||
info.group({metadata, name:"base", inputs:base})
|
||||
info("Base sections", parts)
|
||||
base.base = false
|
||||
for (const part of conf.settings.plugins.base.parts)
|
||||
base[`base.${part}`] = parts.includes(part)
|
||||
Object.assign(q, base)
|
||||
|
||||
//Additional plugins
|
||||
const plugins = {}
|
||||
for (const name of Object.keys(Plugins).filter(key => !["base", "core"].includes(key))) {
|
||||
//Parse inputs
|
||||
const {[name]:enabled, ...inputs} = metadata.plugins[name].inputs.action({core})
|
||||
plugins[name] = {enabled}
|
||||
//Register user inputs
|
||||
if (enabled) {
|
||||
info.break()
|
||||
info.group({metadata, name, inputs})
|
||||
q[name] = true
|
||||
for (const [key, value] of Object.entries(inputs)) {
|
||||
//Store token in plugin configuration
|
||||
if (metadata.plugins[name].inputs[key].type === "token")
|
||||
plugins[name][key] = value
|
||||
//Store value in query
|
||||
else
|
||||
info("Dry-run", true)
|
||||
q[`${name}.${key}`] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//SVG file
|
||||
conf.settings.optimize = optimize
|
||||
info("SVG output", filename)
|
||||
info("SVG optimization", optimize)
|
||||
info("SVG verification after generation", verify)
|
||||
//Render metrics
|
||||
info.break()
|
||||
info.section("Rendering")
|
||||
let error = null, rendered = null
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
console.debug(`::group::Attempt ${attempt}/${retries}`)
|
||||
;({rendered} = await metrics({login:user, q}, {graphql, rest, plugins, conf, die, verify, convert}, {Plugins, Templates}))
|
||||
console.debug("::endgroup::")
|
||||
break
|
||||
}
|
||||
catch (_error) {
|
||||
error = _error
|
||||
console.debug("::endgroup::")
|
||||
console.debug(`::warning::rendering failed (${error.message})`)
|
||||
await wait(retries_delay)
|
||||
}
|
||||
}
|
||||
if (!rendered)
|
||||
throw error ?? new Error("Could not render metrics")
|
||||
info("Status", "complete")
|
||||
|
||||
//Template
|
||||
info.break()
|
||||
info.section("Templates")
|
||||
info("Community templates", _templates)
|
||||
info("Template used", template)
|
||||
info("Query additional params", query)
|
||||
//Save output to renders output folder
|
||||
info.break()
|
||||
info.section("Saving")
|
||||
if (dryrun)
|
||||
info("Actions to perform", "(none)")
|
||||
else {
|
||||
await fs.mkdir(paths.dirname(paths.join("/renders", filename)), {recursive:true})
|
||||
await fs.writeFile(paths.join("/renders", filename), Buffer.from(rendered))
|
||||
info(`Save to /metrics_renders/${filename}`, "ok")
|
||||
}
|
||||
|
||||
//Core config
|
||||
info.break()
|
||||
info.group({metadata, name:"core", inputs:config})
|
||||
info("Plugin errors", die ? "(exit with error)" : "(displayed in generated image)")
|
||||
const convert = _output || null
|
||||
Object.assign(q, config)
|
||||
if (/markdown/.test(convert))
|
||||
info("Markdown cache", _markdown_cache)
|
||||
|
||||
//Base content
|
||||
info.break()
|
||||
const {base:parts, ...base} = metadata.plugins.base.inputs.action({core})
|
||||
info.group({metadata, name:"base", inputs:base})
|
||||
info("Base sections", parts)
|
||||
base.base = false
|
||||
for (const part of conf.settings.plugins.base.parts)
|
||||
base[`base.${part}`] = parts.includes(part)
|
||||
Object.assign(q, base)
|
||||
|
||||
//Additional plugins
|
||||
const plugins = {}
|
||||
for (const name of Object.keys(Plugins).filter(key => !["base", "core"].includes(key))) {
|
||||
//Parse inputs
|
||||
const {[name]:enabled, ...inputs} = metadata.plugins[name].inputs.action({core})
|
||||
plugins[name] = {enabled}
|
||||
//Register user inputs
|
||||
if (enabled) {
|
||||
info.break()
|
||||
info.group({metadata, name, inputs})
|
||||
q[name] = true
|
||||
for (const [key, value] of Object.entries(inputs)) {
|
||||
//Store token in plugin configuration
|
||||
if (metadata.plugins[name].inputs[key].type === "token")
|
||||
plugins[name][key] = value
|
||||
//Store value in query
|
||||
else
|
||||
q[`${name}.${key}`] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Render metrics
|
||||
info.break()
|
||||
info.section("Rendering")
|
||||
let error = null, rendered = null
|
||||
for (let attempt = 1; attempt <= retries; attempt++) {
|
||||
try {
|
||||
console.debug(`::group::Attempt ${attempt}/${retries}`)
|
||||
;({rendered} = await metrics({login:user, q}, {graphql, rest, plugins, conf, die, verify, convert}, {Plugins, Templates}))
|
||||
console.debug("::endgroup::")
|
||||
break
|
||||
}
|
||||
catch (_error) {
|
||||
error = _error
|
||||
console.debug("::endgroup::")
|
||||
console.debug(`::warning::rendering failed (${error.message})`)
|
||||
await wait(retries_delay)
|
||||
}
|
||||
}
|
||||
if (!rendered)
|
||||
throw error ?? new Error("Could not render metrics")
|
||||
info("Status", "complete")
|
||||
|
||||
//Save output to renders output folder
|
||||
info.break()
|
||||
info.section("Saving")
|
||||
if (dryrun)
|
||||
info("Actions to perform", "(none)")
|
||||
else {
|
||||
await fs.mkdir(paths.dirname(paths.join("/renders", filename)), {recursive:true})
|
||||
await fs.writeFile(paths.join("/renders", filename), Buffer.from(rendered))
|
||||
info(`Save to /metrics_renders/${filename}`, "ok")
|
||||
}
|
||||
|
||||
//Cache
|
||||
if (/markdown/.test(convert)) {
|
||||
const regex = /(?<match><img class="metrics-cachable" data-name="(?<name>[\s\S]+?)" src="data:image[/]svg[+]xml;base64,(?<content>[/+=\w]+)">)/g
|
||||
let matched = null
|
||||
while (matched = regex.exec(rendered)?.groups) { //eslint-disable-line no-cond-assign
|
||||
const {match, name, content} = matched
|
||||
let path = `${_markdown_cache}/${name}.svg`
|
||||
console.debug(`Processing ${path}`)
|
||||
let sha = null
|
||||
try {
|
||||
const {repository:{object:{oid}}} = await graphql(`
|
||||
//Cache
|
||||
if (/markdown/.test(convert)) {
|
||||
const regex = /(?<match><img class="metrics-cachable" data-name="(?<name>[\s\S]+?)" src="data:image[/]svg[+]xml;base64,(?<content>[/+=\w]+)">)/g
|
||||
let matched = null
|
||||
while (matched = regex.exec(rendered)?.groups) { //eslint-disable-line no-cond-assign
|
||||
const {match, name, content} = matched
|
||||
let path = `${_markdown_cache}/${name}.svg`
|
||||
console.debug(`Processing ${path}`)
|
||||
let sha = null
|
||||
try {
|
||||
const {repository:{object:{oid}}} = await graphql(
|
||||
`
|
||||
query Sha {
|
||||
repository(owner: "${github.context.repo.owner}", name: "${github.context.repo.repo}") {
|
||||
object(expression: "${committer.head}:${path}") { ... on Blob { oid } }
|
||||
}
|
||||
}
|
||||
`, {headers:{authorization:`token ${committer.token}`}})
|
||||
sha = oid
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
}
|
||||
finally {
|
||||
await committer.rest.repos.createOrUpdateFileContents({
|
||||
...github.context.repo, path, content,
|
||||
message:`${committer.message} (cache)`, ...(sha ? {sha} : {}),
|
||||
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}">`)
|
||||
info(`Saving ${path}`, "ok")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Check editions
|
||||
if ((committer.commit)||(committer.pr)) {
|
||||
const git = sgit()
|
||||
const sha = await git.hashObject(paths.join("/renders", filename))
|
||||
info("Current render sha", sha)
|
||||
if (committer.sha === sha) {
|
||||
info(`Commit to branch ${committer.branch}`, "(no changes)")
|
||||
committer.commit = false
|
||||
}
|
||||
}
|
||||
|
||||
//Upload to gist (this is done as user since committer_token may not have gist rights)
|
||||
if (committer.gist) {
|
||||
await rest.gists.update({gist_id:committer.gist, files:{[filename]:{content:rendered}}})
|
||||
info(`Upload to gist ${committer.gist}`, "ok")
|
||||
committer.commit = false
|
||||
}
|
||||
|
||||
//Commit metrics
|
||||
if (committer.commit) {
|
||||
await committer.rest.repos.createOrUpdateFileContents({
|
||||
...github.context.repo, path:filename, message:committer.message,
|
||||
content:Buffer.from(rendered).toString("base64"),
|
||||
branch:committer.pr ? committer.head : committer.branch,
|
||||
...(committer.sha ? {sha:committer.sha} : {}),
|
||||
})
|
||||
info(`Commit to branch ${committer.branch}`, "ok")
|
||||
}
|
||||
|
||||
//Pull request
|
||||
if (committer.pr) {
|
||||
//Create pull request
|
||||
let number = null
|
||||
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}))
|
||||
info(`Pull request from ${committer.head} to ${committer.branch}`, "(created)")
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
//Check if pull request has already been created previously
|
||||
if (/A pull request already exists/.test(error)) {
|
||||
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 prs = (await committer.rest.search.issuesAndPullRequests({q})).data.items.filter(({user:{login}}) => login === "github-actions[bot]")
|
||||
if (prs.length < 1)
|
||||
throw new Error("0 matching prs. Cannot proceed.")
|
||||
if (prs.length > 1)
|
||||
throw new Error(`Found more than one matching prs: ${prs.map(({number}) => `#${number}`).join(", ")}. Cannot proceed.`)
|
||||
;({number} = prs.shift())
|
||||
}
|
||||
//Check if pull request could not been created because there are no diff between head and base
|
||||
else if (/No commits between/.test(error)) {
|
||||
info(`Pull request from ${committer.head} to ${committer.branch}`, "(no diff)")
|
||||
committer.merge = false
|
||||
number = "(none)"
|
||||
}
|
||||
else
|
||||
throw error
|
||||
}
|
||||
info("Pull request number", number)
|
||||
//Merge pull request
|
||||
if (committer.merge) {
|
||||
info("Merge method", committer.merge)
|
||||
let attempts = 240
|
||||
do {
|
||||
//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})
|
||||
console.debug(`Pull request #${number} mergeable state is "${state}"`)
|
||||
if (mergeable === null) {
|
||||
await wait(15)
|
||||
continue
|
||||
}
|
||||
if (!mergeable)
|
||||
throw new Error(`Pull request #${number} is not mergeable (state is "${state}")`)
|
||||
//Merge pull request
|
||||
await committer.rest.pulls.merge({...github.context.repo, pull_number:number, merge_method:committer.merge})
|
||||
info(`Merge #${number} to ${committer.branch}`, "ok")
|
||||
//Delete head branch
|
||||
try {
|
||||
await wait(15)
|
||||
await committer.rest.git.deleteRef({...github.context.repo, ref:`heads/${committer.head}`})
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
if (!/reference does not exist/i.test(`${error}`))
|
||||
throw error
|
||||
}
|
||||
info(`Branch ${committer.head}`, "(deleted)")
|
||||
break
|
||||
} while (--attempts)
|
||||
}
|
||||
}
|
||||
|
||||
//Success
|
||||
info.break()
|
||||
console.log("Success, thanks for using metrics!")
|
||||
process.exit(0)
|
||||
`,
|
||||
{headers:{authorization:`token ${committer.token}`}},
|
||||
)
|
||||
sha = oid
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
}
|
||||
finally {
|
||||
await committer.rest.repos.createOrUpdateFileContents({
|
||||
...github.context.repo,
|
||||
path,
|
||||
content,
|
||||
message:`${committer.message} (cache)`,
|
||||
...(sha ? {sha} : {}),
|
||||
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}">`)
|
||||
info(`Saving ${path}`, "ok")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Check editions
|
||||
if ((committer.commit) || (committer.pr)) {
|
||||
const git = sgit()
|
||||
const sha = await git.hashObject(paths.join("/renders", filename))
|
||||
info("Current render sha", sha)
|
||||
if (committer.sha === sha) {
|
||||
info(`Commit to branch ${committer.branch}`, "(no changes)")
|
||||
committer.commit = false
|
||||
}
|
||||
}
|
||||
|
||||
//Upload to gist (this is done as user since committer_token may not have gist rights)
|
||||
if (committer.gist) {
|
||||
await rest.gists.update({gist_id:committer.gist, files:{[filename]:{content:rendered}}})
|
||||
info(`Upload to gist ${committer.gist}`, "ok")
|
||||
committer.commit = false
|
||||
}
|
||||
|
||||
//Commit metrics
|
||||
if (committer.commit) {
|
||||
await committer.rest.repos.createOrUpdateFileContents({
|
||||
...github.context.repo,
|
||||
path:filename,
|
||||
message:committer.message,
|
||||
content:Buffer.from(rendered).toString("base64"),
|
||||
branch:committer.pr ? committer.head : committer.branch,
|
||||
...(committer.sha ? {sha:committer.sha} : {}),
|
||||
})
|
||||
info(`Commit to branch ${committer.branch}`, "ok")
|
||||
}
|
||||
|
||||
//Pull request
|
||||
if (committer.pr) {
|
||||
//Create pull request
|
||||
let number = null
|
||||
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}))
|
||||
info(`Pull request from ${committer.head} to ${committer.branch}`, "(created)")
|
||||
}
|
||||
//Errors
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
//Print debug buffer if debug was not enabled (if it is, it's already logged on the fly)
|
||||
if (!DEBUG) {
|
||||
for (const log of [info.break(), "An error occured, logging debug message :", ...debugged])
|
||||
console.log(log)
|
||||
}
|
||||
core.setFailed(error.message)
|
||||
process.exit(1)
|
||||
console.debug(error)
|
||||
//Check if pull request has already been created previously
|
||||
if (/A pull request already exists/.test(error)) {
|
||||
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 prs = (await committer.rest.search.issuesAndPullRequests({q})).data.items.filter(({user:{login}}) => login === "github-actions[bot]")
|
||||
if (prs.length < 1)
|
||||
throw new Error("0 matching prs. Cannot proceed.")
|
||||
if (prs.length > 1)
|
||||
throw new Error(`Found more than one matching prs: ${prs.map(({number}) => `#${number}`).join(", ")}. Cannot proceed.`)
|
||||
;({number} = prs.shift())
|
||||
}
|
||||
//Check if pull request could not been created because there are no diff between head and base
|
||||
else if (/No commits between/.test(error)) {
|
||||
info(`Pull request from ${committer.head} to ${committer.branch}`, "(no diff)")
|
||||
committer.merge = false
|
||||
number = "(none)"
|
||||
}
|
||||
else
|
||||
throw error
|
||||
|
||||
}
|
||||
})()
|
||||
info("Pull request number", number)
|
||||
//Merge pull request
|
||||
if (committer.merge) {
|
||||
info("Merge method", committer.merge)
|
||||
let attempts = 240
|
||||
do {
|
||||
//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})
|
||||
console.debug(`Pull request #${number} mergeable state is "${state}"`)
|
||||
if (mergeable === null) {
|
||||
await wait(15)
|
||||
continue
|
||||
}
|
||||
if (!mergeable)
|
||||
throw new Error(`Pull request #${number} is not mergeable (state is "${state}")`)
|
||||
//Merge pull request
|
||||
await committer.rest.pulls.merge({...github.context.repo, pull_number:number, merge_method:committer.merge})
|
||||
info(`Merge #${number} to ${committer.branch}`, "ok")
|
||||
//Delete head branch
|
||||
try {
|
||||
await wait(15)
|
||||
await committer.rest.git.deleteRef({...github.context.repo, ref:`heads/${committer.head}`})
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
if (!/reference does not exist/i.test(`${error}`))
|
||||
throw error
|
||||
}
|
||||
info(`Branch ${committer.head}`, "(deleted)")
|
||||
break
|
||||
} while (--attempts)
|
||||
}
|
||||
}
|
||||
|
||||
//Success
|
||||
info.break()
|
||||
console.log("Success, thanks for using metrics!")
|
||||
process.exit(0)
|
||||
}
|
||||
//Errors
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
//Print debug buffer if debug was not enabled (if it is, it's already logged on the fly)
|
||||
if (!DEBUG) {
|
||||
for (const log of [info.break(), "An error occured, logging debug message :", ...debugged])
|
||||
console.log(log)
|
||||
}
|
||||
core.setFailed(error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
})()
|
||||
|
||||
@@ -1,193 +1,205 @@
|
||||
//Imports
|
||||
import * as utils from "./utils.mjs"
|
||||
import ejs from "ejs"
|
||||
import util from "util"
|
||||
import SVGO from "svgo"
|
||||
import xmlformat from "xml-formatter"
|
||||
import ejs from "ejs"
|
||||
import SVGO from "svgo"
|
||||
import util from "util"
|
||||
import xmlformat from "xml-formatter"
|
||||
import * as utils from "./utils.mjs"
|
||||
|
||||
//Setup
|
||||
export default async function metrics({login, q}, {graphql, rest, plugins, conf, die = false, verify = false, convert = null}, {Plugins, Templates}) {
|
||||
//Compute rendering
|
||||
try {
|
||||
export default async function metrics({login, q}, {graphql, rest, plugins, conf, die = false, verify = false, convert = null}, {Plugins, Templates}) {
|
||||
//Compute rendering
|
||||
try {
|
||||
//Debug
|
||||
login = login.replace(/[\n\r]/g, "")
|
||||
console.debug(`metrics/compute/${login} > start`)
|
||||
console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256}))
|
||||
|
||||
//Debug
|
||||
login = login.replace(/[\n\r]/g, "")
|
||||
console.debug(`metrics/compute/${login} > start`)
|
||||
console.debug(util.inspect(q, {depth:Infinity, maxStringLength:256}))
|
||||
//Load template
|
||||
const template = q.template || conf.settings.templates.default
|
||||
if ((!(template in Templates)) || (!(template in conf.templates)) || ((conf.settings.templates.enabled.length) && (!conf.settings.templates.enabled.includes(template))))
|
||||
throw new Error("unsupported template")
|
||||
const {image, style, fonts, views, partials} = conf.templates[template]
|
||||
const computer = Templates[template].default || Templates[template]
|
||||
convert = convert ?? conf.metadata.templates[template].formats[0] ?? null
|
||||
console.debug(`metrics/compute/${login} > output format set to ${convert}`)
|
||||
|
||||
//Load template
|
||||
const template = q.template || conf.settings.templates.default
|
||||
if ((!(template in Templates))||(!(template in conf.templates))||((conf.settings.templates.enabled.length)&&(!conf.settings.templates.enabled.includes(template))))
|
||||
throw new Error("unsupported template")
|
||||
const {image, style, fonts, views, partials} = conf.templates[template]
|
||||
const computer = Templates[template].default || Templates[template]
|
||||
convert = convert ?? conf.metadata.templates[template].formats[0] ?? null
|
||||
console.debug(`metrics/compute/${login} > output format set to ${convert}`)
|
||||
|
||||
//Initialization
|
||||
const pending = []
|
||||
const {queries} = conf
|
||||
const data = {animated:true, base:{}, config:{}, errors:[], plugins:{}, computed:{}}
|
||||
const imports = {plugins:Plugins, templates:Templates, metadata:conf.metadata, ...utils, ...(/markdown/.test(convert) ? {imgb64(url, options) {
|
||||
//Initialization
|
||||
const pending = []
|
||||
const {queries} = conf
|
||||
const data = {animated:true, base:{}, config:{}, errors:[], plugins:{}, computed:{}}
|
||||
const imports = {
|
||||
plugins:Plugins,
|
||||
templates:Templates,
|
||||
metadata:conf.metadata,
|
||||
...utils,
|
||||
...(/markdown/.test(convert)
|
||||
? {
|
||||
imgb64(url, options) {
|
||||
return options?.force ? utils.imgb64(...arguments) : url
|
||||
}} : null)}
|
||||
const experimental = new Set(decodeURIComponent(q["experimental.features"] ?? "").split(" ").map(x => x.trim().toLocaleLowerCase()).filter(x => x))
|
||||
if (conf.settings["debug.headless"])
|
||||
imports.puppeteer.headless = false
|
||||
},
|
||||
}
|
||||
: null),
|
||||
}
|
||||
const experimental = new Set(decodeURIComponent(q["experimental.features"] ?? "").split(" ").map(x => x.trim().toLocaleLowerCase()).filter(x => x))
|
||||
if (conf.settings["debug.headless"])
|
||||
imports.puppeteer.headless = false
|
||||
|
||||
//Partial parts
|
||||
{
|
||||
data.partials = new Set([
|
||||
...decodeURIComponent(q["config.order"] ?? "").split(",").map(x => x.trim().toLocaleLowerCase()).filter(partial => partials.includes(partial)),
|
||||
...partials,
|
||||
])
|
||||
console.debug(`metrics/compute/${login} > content order : ${[...data.partials]}`)
|
||||
}
|
||||
//Partial parts
|
||||
{
|
||||
data.partials = new Set([
|
||||
...decodeURIComponent(q["config.order"] ?? "").split(",").map(x => x.trim().toLocaleLowerCase()).filter(partial => partials.includes(partial)),
|
||||
...partials,
|
||||
])
|
||||
console.debug(`metrics/compute/${login} > content order : ${[...data.partials]}`)
|
||||
}
|
||||
|
||||
//Executing base plugin and compute metrics
|
||||
console.debug(`metrics/compute/${login} > compute`)
|
||||
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})
|
||||
const promised = await Promise.all(pending)
|
||||
//Executing base plugin and compute metrics
|
||||
console.debug(`metrics/compute/${login} > compute`)
|
||||
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})
|
||||
const promised = await Promise.all(pending)
|
||||
|
||||
//Check plugins errors
|
||||
const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors]
|
||||
if (errors.length) {
|
||||
console.warn(`metrics/compute/${login} > ${errors.length} errors !`)
|
||||
if (die)
|
||||
throw new Error("An error occured during rendering, dying")
|
||||
else
|
||||
console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256}))
|
||||
}
|
||||
//Check plugins errors
|
||||
const errors = [...promised.filter(({result = null}) => result?.error), ...data.errors]
|
||||
if (errors.length) {
|
||||
console.warn(`metrics/compute/${login} > ${errors.length} errors !`)
|
||||
if (die)
|
||||
throw new Error("An error occured during rendering, dying")
|
||||
else
|
||||
console.warn(util.inspect(errors, {depth:Infinity, maxStringLength:256}))
|
||||
}
|
||||
|
||||
//JSON output
|
||||
if (convert === "json") {
|
||||
console.debug(`metrics/compute/${login} > json output`)
|
||||
return {rendered:data, mime:"application/json"}
|
||||
}
|
||||
//JSON output
|
||||
if (convert === "json") {
|
||||
console.debug(`metrics/compute/${login} > json output`)
|
||||
return {rendered:data, mime:"application/json"}
|
||||
}
|
||||
|
||||
//Markdown output
|
||||
if (/markdown/.test(convert)) {
|
||||
//Retrieving template source
|
||||
console.debug(`metrics/compute/${login} > markdown render`)
|
||||
let source = image
|
||||
try {
|
||||
let template = `${q.markdown}`.replace(/\n/g, "")
|
||||
if (!/^https:/.test(template)) {
|
||||
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}`)
|
||||
template = `https://raw.githubusercontent.com/${repo}/${branch}/${template}`
|
||||
}
|
||||
console.debug(`metrics/compute/${login} > fetching ${template}`)
|
||||
;({data:source} = await imports.axios.get(template, {headers:{Accept:"text/plain"}}))
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(error)
|
||||
}
|
||||
//Embed method
|
||||
const embed = async(name, q = {}) => {
|
||||
//Check arguments
|
||||
if ((!name)||(typeof q !== "object")||(q === null)) {
|
||||
if (die)
|
||||
throw new Error("An error occured during embed rendering, dying")
|
||||
return "<p>⚠️ Failed to execute embed function: invalid arguments</p>"
|
||||
}
|
||||
//Translate action syntax to web syntax
|
||||
let parts = []
|
||||
if (q.base === true)
|
||||
({parts} = conf.settings.plugins.base)
|
||||
if (typeof q.base === "string")
|
||||
parts = q.base.split(",").map(x => x.trim())
|
||||
if (Array.isArray(q.base))
|
||||
parts = q.base
|
||||
for (const part of conf.settings.plugins.base.parts)
|
||||
q[`base.${part}`] = q[`base.${part}`] ?? parts.includes(part)
|
||||
if (convert === "markdown-pdf") {
|
||||
q["config.animations"] = false
|
||||
q.config_animations = false
|
||||
}
|
||||
q = Object.fromEntries([...Object.entries(q).map(([key, value]) => [key.replace(/^plugin_/, "").replace(/_/g, "."), value]), ["base", false]])
|
||||
//Enable required plugins
|
||||
const plugins = Object.fromEntries(Object.entries(arguments[1].plugins).map(([key, value]) => [key, {...value, enabled:true}]))
|
||||
//Compute rendering
|
||||
const {rendered} = await metrics({login, q}, {...arguments[1], plugins, convert:null}, arguments[2])
|
||||
return `<img class="metrics-cachable" data-name="${name}" src="data:image/svg+xml;base64,${Buffer.from(rendered).toString("base64")}">`
|
||||
}
|
||||
//Rendering template source
|
||||
let rendered = source.replace(/\{\{ (?<content>[\s\S]*?) \}\}/g, "{%= $<content> %}")
|
||||
console.debug(rendered)
|
||||
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})
|
||||
console.debug(`metrics/compute/${login} > success`)
|
||||
//Output
|
||||
if (convert === "markdown-pdf") {
|
||||
return imports.svg.pdf(rendered, {
|
||||
paddings:q["config.padding"] || conf.settings.padding,
|
||||
style:(conf.settings.extras?.css ?? conf.settings.extras?.default ? q["extras.css"] ?? "" : ""),
|
||||
twemojis:q["config.twemoji"],
|
||||
gemojis:q["config.gemoji"],
|
||||
rest,
|
||||
})
|
||||
}
|
||||
return {rendered, mime:"text/html"}
|
||||
}
|
||||
|
||||
//Rendering
|
||||
console.debug(`metrics/compute/${login} > render`)
|
||||
let rendered = await ejs.render(image, {...data, s:imports.s, f:imports.format, style:style+(conf.settings.extras?.css ?? conf.settings.extras?.default ? q["extras.css"] ?? "" : ""), fonts}, {views, async:true})
|
||||
|
||||
//Additional transformations
|
||||
if (q["config.twemoji"])
|
||||
rendered = await imports.svg.twemojis(rendered)
|
||||
if (q["config.gemoji"])
|
||||
rendered = await imports.svg.gemojis(rendered, {rest})
|
||||
//Optimize rendering
|
||||
if (!q.raw)
|
||||
rendered = xmlformat(rendered, {lineSeparator:"\n", collapseContent:true})
|
||||
if ((conf.settings?.optimize)&&(!q.raw)) {
|
||||
console.debug(`metrics/compute/${login} > optimize`)
|
||||
if (experimental.has("--optimize")) {
|
||||
const {error, data:optimized} = await SVGO.optimize(rendered, {multipass:true, plugins:SVGO.extendDefaultPlugins([
|
||||
//Additional cleanup
|
||||
{name:"cleanupListOfValues"},
|
||||
{name:"removeRasterImages"},
|
||||
{name:"removeScriptElement"},
|
||||
//Force CSS style consistency
|
||||
{name:"inlineStyles", active:false},
|
||||
{name:"removeViewBox", active:false},
|
||||
])})
|
||||
if (error)
|
||||
throw new Error(`Could not optimize SVG: \n${error}`)
|
||||
rendered = optimized
|
||||
console.debug(`metrics/compute/${login} > optimize > success`)
|
||||
}
|
||||
else
|
||||
console.debug(`metrics/compute/${login} > optimize > this feature is currently disabled due to display issues (use --optimize flag in experimental features to force enable it)`)
|
||||
}
|
||||
//Verify svg
|
||||
if (verify) {
|
||||
console.debug(`metrics/compute/${login} > verify SVG`)
|
||||
const libxmljs = (await import("libxmljs2")).default
|
||||
const parsed = libxmljs.parseXml(rendered)
|
||||
if (parsed.errors.length)
|
||||
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
|
||||
console.debug(`metrics/compute/${login} > verified SVG, no parsing errors found`)
|
||||
}
|
||||
//Resizing
|
||||
const {resized, mime} = await imports.svg.resize(rendered, {paddings:q["config.padding"] || conf.settings.padding, convert:convert === "svg" ? null : convert})
|
||||
rendered = resized
|
||||
|
||||
//Result
|
||||
console.debug(`metrics/compute/${login} > success`)
|
||||
return {rendered, mime}
|
||||
//Markdown output
|
||||
if (/markdown/.test(convert)) {
|
||||
//Retrieving template source
|
||||
console.debug(`metrics/compute/${login} > markdown render`)
|
||||
let source = image
|
||||
try {
|
||||
let template = `${q.markdown}`.replace(/\n/g, "")
|
||||
if (!/^https:/.test(template)) {
|
||||
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}`)
|
||||
template = `https://raw.githubusercontent.com/${repo}/${branch}/${template}`
|
||||
}
|
||||
console.debug(`metrics/compute/${login} > fetching ${template}`)
|
||||
;({data:source} = await imports.axios.get(template, {headers:{Accept:"text/plain"}}))
|
||||
}
|
||||
//Internal error
|
||||
catch (error) {
|
||||
//User not found
|
||||
if (((Array.isArray(error.errors))&&(error.errors[0].type === "NOT_FOUND")))
|
||||
throw new Error("user not found")
|
||||
//Generic error
|
||||
throw error
|
||||
console.debug(error)
|
||||
}
|
||||
}
|
||||
//Embed method
|
||||
const embed = async (name, q = {}) => {
|
||||
//Check arguments
|
||||
if ((!name) || (typeof q !== "object") || (q === null)) {
|
||||
if (die)
|
||||
throw new Error("An error occured during embed rendering, dying")
|
||||
return "<p>⚠️ Failed to execute embed function: invalid arguments</p>"
|
||||
}
|
||||
//Translate action syntax to web syntax
|
||||
let parts = []
|
||||
if (q.base === true);
|
||||
({parts} = conf.settings.plugins.base)
|
||||
if (typeof q.base === "string")
|
||||
parts = q.base.split(",").map(x => x.trim())
|
||||
if (Array.isArray(q.base))
|
||||
parts = q.base
|
||||
for (const part of conf.settings.plugins.base.parts)
|
||||
q[`base.${part}`] = q[`base.${part}`] ?? parts.includes(part)
|
||||
if (convert === "markdown-pdf") {
|
||||
q["config.animations"] = false
|
||||
q.config_animations = false
|
||||
}
|
||||
q = Object.fromEntries([...Object.entries(q).map(([key, value]) => [key.replace(/^plugin_/, "").replace(/_/g, "."), value]), ["base", false]])
|
||||
//Enable required plugins
|
||||
const plugins = Object.fromEntries(Object.entries(arguments[1].plugins).map(([key, value]) => [key, {...value, enabled:true}]))
|
||||
//Compute rendering
|
||||
const {rendered} = await metrics({login, q}, {...arguments[1], plugins, convert:null}, arguments[2])
|
||||
return `<img class="metrics-cachable" data-name="${name}" src="data:image/svg+xml;base64,${Buffer.from(rendered).toString("base64")}">`
|
||||
}
|
||||
//Rendering template source
|
||||
let rendered = source.replace(/\{\{ (?<content>[\s\S]*?) \}\}/g, "{%= $<content> %}")
|
||||
console.debug(rendered)
|
||||
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})
|
||||
console.debug(`metrics/compute/${login} > success`)
|
||||
//Output
|
||||
if (convert === "markdown-pdf") {
|
||||
return imports.svg.pdf(rendered, {
|
||||
paddings:q["config.padding"] || conf.settings.padding,
|
||||
style:(conf.settings.extras?.css ?? conf.settings.extras?.default ? q["extras.css"] ?? "" : ""),
|
||||
twemojis:q["config.twemoji"],
|
||||
gemojis:q["config.gemoji"],
|
||||
rest,
|
||||
})
|
||||
}
|
||||
return {rendered, mime:"text/html"}
|
||||
}
|
||||
|
||||
//Rendering
|
||||
console.debug(`metrics/compute/${login} > render`)
|
||||
let rendered = await ejs.render(image, {...data, s:imports.s, f:imports.format, style:style + (conf.settings.extras?.css ?? conf.settings.extras?.default ? q["extras.css"] ?? "" : ""), fonts}, {views, async:true})
|
||||
|
||||
//Additional transformations
|
||||
if (q["config.twemoji"])
|
||||
rendered = await imports.svg.twemojis(rendered)
|
||||
if (q["config.gemoji"])
|
||||
rendered = await imports.svg.gemojis(rendered, {rest})
|
||||
//Optimize rendering
|
||||
if (!q.raw)
|
||||
rendered = xmlformat(rendered, {lineSeparator:"\n", collapseContent:true})
|
||||
if ((conf.settings?.optimize) && (!q.raw)) {
|
||||
console.debug(`metrics/compute/${login} > optimize`)
|
||||
if (experimental.has("--optimize")) {
|
||||
const {error, data:optimized} = await SVGO.optimize(rendered, {
|
||||
multipass:true,
|
||||
plugins:SVGO.extendDefaultPlugins([
|
||||
//Additional cleanup
|
||||
{name:"cleanupListOfValues"},
|
||||
{name:"removeRasterImages"},
|
||||
{name:"removeScriptElement"},
|
||||
//Force CSS style consistency
|
||||
{name:"inlineStyles", active:false},
|
||||
{name:"removeViewBox", active:false},
|
||||
]),
|
||||
})
|
||||
if (error)
|
||||
throw new Error(`Could not optimize SVG: \n${error}`)
|
||||
rendered = optimized
|
||||
console.debug(`metrics/compute/${login} > optimize > success`)
|
||||
}
|
||||
else
|
||||
console.debug(`metrics/compute/${login} > optimize > this feature is currently disabled due to display issues (use --optimize flag in experimental features to force enable it)`)
|
||||
|
||||
}
|
||||
//Verify svg
|
||||
if (verify) {
|
||||
console.debug(`metrics/compute/${login} > verify SVG`)
|
||||
const libxmljs = (await import("libxmljs2")).default
|
||||
const parsed = libxmljs.parseXml(rendered)
|
||||
if (parsed.errors.length)
|
||||
throw new Error(`Malformed SVG : \n${parsed.errors.join("\n")}`)
|
||||
console.debug(`metrics/compute/${login} > verified SVG, no parsing errors found`)
|
||||
}
|
||||
//Resizing
|
||||
const {resized, mime} = await imports.svg.resize(rendered, {paddings:q["config.padding"] || conf.settings.padding, convert:convert === "svg" ? null : convert})
|
||||
rendered = resized
|
||||
|
||||
//Result
|
||||
console.debug(`metrics/compute/${login} > success`)
|
||||
return {rendered, mime}
|
||||
}
|
||||
//Internal error
|
||||
catch (error) {
|
||||
//User not found
|
||||
if (((Array.isArray(error.errors)) && (error.errors[0].type === "NOT_FOUND")))
|
||||
throw new Error("user not found")
|
||||
//Generic error
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,307 +1,313 @@
|
||||
//Imports
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import url from "url"
|
||||
import yaml from "js-yaml"
|
||||
import fs from "fs"
|
||||
import yaml from "js-yaml"
|
||||
import path from "path"
|
||||
import url from "url"
|
||||
|
||||
//Defined categories
|
||||
const categories = ["core", "github", "social", "other"]
|
||||
const categories = ["core", "github", "social", "other"]
|
||||
|
||||
/**Metadata descriptor parser */
|
||||
export default async function metadata({log = true} = {}) {
|
||||
//Paths
|
||||
const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../../..")
|
||||
const __templates = path.join(__metrics, "source/templates")
|
||||
const __plugins = path.join(__metrics, "source/plugins")
|
||||
const __package = path.join(__metrics, "package.json")
|
||||
export default async function metadata({log = true} = {}) {
|
||||
//Paths
|
||||
const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../../..")
|
||||
const __templates = path.join(__metrics, "source/templates")
|
||||
const __plugins = path.join(__metrics, "source/plugins")
|
||||
const __package = path.join(__metrics, "package.json")
|
||||
|
||||
//Init
|
||||
const logger = log ? console.debug : () => null
|
||||
//Init
|
||||
const logger = log ? console.debug : () => null
|
||||
|
||||
//Load plugins metadata
|
||||
let Plugins = {}
|
||||
logger("metrics/metadata > loading plugins metadata")
|
||||
for (const name of await fs.promises.readdir(__plugins)) {
|
||||
if (!(await fs.promises.lstat(path.join(__plugins, name))).isDirectory())
|
||||
continue
|
||||
logger(`metrics/metadata > loading plugin metadata [${name}]`)
|
||||
Plugins[name] = await metadata.plugin({__plugins, name, logger})
|
||||
}
|
||||
//Reorder keys
|
||||
const {base, core, ...plugins} = Plugins //eslint-disable-line no-unused-vars
|
||||
Plugins = Object.fromEntries(Object.entries(Plugins).sort(([_an, a], [_bn, b]) => a.categorie === b.categorie ? (a.index ?? Infinity) - (b.index ?? Infinity) : categories.indexOf(a.categorie) - categories.indexOf(b.categorie)))
|
||||
logger(`metrics/metadata > loaded [${Object.keys(Plugins).join(", ")}]`)
|
||||
//Load templates metadata
|
||||
let Templates = {}
|
||||
logger("metrics/metadata > loading templates metadata")
|
||||
for (const name of await fs.promises.readdir(__templates)) {
|
||||
if (!(await fs.promises.lstat(path.join(__templates, name))).isDirectory())
|
||||
continue
|
||||
if (/^@/.test(name))
|
||||
continue
|
||||
logger(`metrics/metadata > loading template metadata [${name}]`)
|
||||
Templates[name] = await metadata.template({__templates, name, plugins, logger})
|
||||
}
|
||||
//Reorder keys
|
||||
const {community, ...templates} = Templates
|
||||
Templates = {...Object.fromEntries(Object.entries(templates).sort(([_an, a], [_bn, b]) => (a.index ?? Infinity) - (b.index ?? Infinity))), community}
|
||||
|
||||
//Packaged metadata
|
||||
const packaged = JSON.parse(`${await fs.promises.readFile(__package)}`)
|
||||
|
||||
//Metadata
|
||||
return {plugins:Plugins, templates:Templates, packaged}
|
||||
//Load plugins metadata
|
||||
let Plugins = {}
|
||||
logger("metrics/metadata > loading plugins metadata")
|
||||
for (const name of await fs.promises.readdir(__plugins)) {
|
||||
if (!(await fs.promises.lstat(path.join(__plugins, name))).isDirectory())
|
||||
continue
|
||||
logger(`metrics/metadata > loading plugin metadata [${name}]`)
|
||||
Plugins[name] = await metadata.plugin({__plugins, name, logger})
|
||||
}
|
||||
//Reorder keys
|
||||
const {base, core, ...plugins} = Plugins //eslint-disable-line no-unused-vars
|
||||
Plugins = Object.fromEntries(Object.entries(Plugins).sort(([_an, a], [_bn, b]) => a.categorie === b.categorie ? (a.index ?? Infinity) - (b.index ?? Infinity) : categories.indexOf(a.categorie) - categories.indexOf(b.categorie)))
|
||||
logger(`metrics/metadata > loaded [${Object.keys(Plugins).join(", ")}]`)
|
||||
//Load templates metadata
|
||||
let Templates = {}
|
||||
logger("metrics/metadata > loading templates metadata")
|
||||
for (const name of await fs.promises.readdir(__templates)) {
|
||||
if (!(await fs.promises.lstat(path.join(__templates, name))).isDirectory())
|
||||
continue
|
||||
if (/^@/.test(name))
|
||||
continue
|
||||
logger(`metrics/metadata > loading template metadata [${name}]`)
|
||||
Templates[name] = await metadata.template({__templates, name, plugins, logger})
|
||||
}
|
||||
//Reorder keys
|
||||
const {community, ...templates} = Templates
|
||||
Templates = {...Object.fromEntries(Object.entries(templates).sort(([_an, a], [_bn, b]) => (a.index ?? Infinity) - (b.index ?? Infinity))), community}
|
||||
|
||||
//Packaged metadata
|
||||
const packaged = JSON.parse(`${await fs.promises.readFile(__package)}`)
|
||||
|
||||
//Metadata
|
||||
return {plugins:Plugins, templates:Templates, packaged}
|
||||
}
|
||||
|
||||
/**Metadata extractor for templates */
|
||||
metadata.plugin = async function({__plugins, name, logger}) {
|
||||
try {
|
||||
//Load meta descriptor
|
||||
const raw = `${await fs.promises.readFile(path.join(__plugins, name, "metadata.yml"), "utf-8")}`
|
||||
const {inputs, ...meta} = yaml.load(raw)
|
||||
metadata.plugin = async function({__plugins, name, logger}) {
|
||||
try {
|
||||
//Load meta descriptor
|
||||
const raw = `${await fs.promises.readFile(path.join(__plugins, name, "metadata.yml"), "utf-8")}`
|
||||
const {inputs, ...meta} = yaml.load(raw)
|
||||
|
||||
//Categorie
|
||||
if (!categories.includes(meta.categorie))
|
||||
meta.categorie = "other"
|
||||
//Categorie
|
||||
if (!categories.includes(meta.categorie))
|
||||
meta.categorie = "other"
|
||||
|
||||
//Inputs parser
|
||||
{
|
||||
meta.inputs = function({data:{user = null} = {}, q, account}, defaults = {}) {
|
||||
//Support check
|
||||
if (!account)
|
||||
logger(`metrics/inputs > account type not set for plugin ${name}!`)
|
||||
if (account !== "bypass") {
|
||||
const context = q.repo ? "repository" : account
|
||||
if (!meta.supports?.includes(context))
|
||||
throw {error:{message:`Not supported for: ${context}`, instance:new Error()}}
|
||||
}
|
||||
//Inputs checks
|
||||
const result = Object.fromEntries(Object.entries(inputs).map(([key, {type, format, default:defaulted, min, max, values}]) => [
|
||||
//Format key
|
||||
metadata.to.query(key, {name}),
|
||||
//Format value
|
||||
(defaulted => {
|
||||
//Default value
|
||||
let value = q[metadata.to.query(key)] ?? q[key] ?? defaulted
|
||||
//Apply type conversion
|
||||
switch (type) {
|
||||
//Booleans
|
||||
case "boolean":{
|
||||
if (/^(?:[Tt]rue|[Oo]n|[Yy]es|1)$/.test(value))
|
||||
return true
|
||||
if (/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(value))
|
||||
return false
|
||||
return defaulted
|
||||
}
|
||||
//Numbers
|
||||
case "number":{
|
||||
value = Number(value)
|
||||
if (!Number.isFinite(value))
|
||||
value = defaulted
|
||||
if (Number.isFinite(min))
|
||||
value = Math.max(min, value)
|
||||
if (Number.isFinite(max))
|
||||
value = Math.min(value, max)
|
||||
return value
|
||||
}
|
||||
//Array
|
||||
case "array":{
|
||||
try {
|
||||
value = decodeURIComponent(value)
|
||||
}
|
||||
catch {
|
||||
logger(`metrics/inputs > failed to decode uri : ${value}`)
|
||||
value = defaulted
|
||||
}
|
||||
const separators = {"comma-separated":",", "space-separated":" "}
|
||||
const separator = separators[[format].flat().filter(s => s in separators)[0]] ?? ","
|
||||
return value.split(separator).map(v => v.trim().toLocaleLowerCase()).filter(v => Array.isArray(values) ? values.includes(v) : true).filter(v => v)
|
||||
}
|
||||
//String
|
||||
case "string":{
|
||||
value = `${value}`.trim()
|
||||
if (user) {
|
||||
if (value === ".user.login")
|
||||
return user.login
|
||||
if (value === ".user.twitter")
|
||||
return user.twitterUsername
|
||||
if (value === ".user.website")
|
||||
return user.websiteUrl
|
||||
}
|
||||
if ((Array.isArray(values))&&(!values.includes(value)))
|
||||
return defaulted
|
||||
return value
|
||||
}
|
||||
//JSON
|
||||
case "json":{
|
||||
try {
|
||||
value = JSON.parse(value)
|
||||
}
|
||||
catch {
|
||||
logger(`metrics/inputs > failed to parse json : ${value}`)
|
||||
value = JSON.parse(defaulted)
|
||||
}
|
||||
return value
|
||||
}
|
||||
//Token
|
||||
case "token":{
|
||||
return value
|
||||
}
|
||||
//Default
|
||||
default:{
|
||||
return value
|
||||
}
|
||||
}
|
||||
})(defaults[key] ?? defaulted),
|
||||
]))
|
||||
logger(`metrics/inputs > ${name} > ${JSON.stringify(result)}`)
|
||||
return result
|
||||
}
|
||||
Object.assign(meta.inputs, inputs, Object.fromEntries(Object.entries(inputs).map(([key, value]) => [metadata.to.query(key, {name}), value])))
|
||||
//Inputs parser
|
||||
{
|
||||
meta.inputs = function({data:{user = null} = {}, q, account}, defaults = {}) {
|
||||
//Support check
|
||||
if (!account)
|
||||
logger(`metrics/inputs > account type not set for plugin ${name}!`)
|
||||
if (account !== "bypass") {
|
||||
const context = q.repo ? "repository" : account
|
||||
if (!meta.supports?.includes(context))
|
||||
throw {error:{message:`Not supported for: ${context}`, instance:new Error()}}
|
||||
}
|
||||
|
||||
//Action metadata
|
||||
{
|
||||
//Extract comments
|
||||
const comments = {}
|
||||
raw.split(/(?:\r?\n){2,}/m)
|
||||
.map(x => x.trim()).filter(x => x)
|
||||
.map(x => x.split("\n").map(y => y.trim()).join("\n"))
|
||||
.map(x => {
|
||||
const input = x.match(new RegExp(`^\\s*(?<input>${Object.keys(inputs).join("|")}):`, "m"))?.groups?.input ?? null
|
||||
if (input)
|
||||
comments[input] = x.match(new RegExp(`(?<comment>[\\s\\S]*?)(?=(?:${Object.keys(inputs).sort((a, b) => b.length - a.length).join("|")}):)`))?.groups?.comment
|
||||
})
|
||||
|
||||
//Action descriptor
|
||||
meta.action = Object.fromEntries(Object.entries(inputs).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
comment:comments[key] ?? "",
|
||||
descriptor:yaml.dump({[key]:Object.fromEntries(Object.entries(value).filter(([key]) => ["description", "default", "required"].includes(key)))}, {quotingType:'"', noCompatMode:true}),
|
||||
},
|
||||
]))
|
||||
|
||||
//Action inputs
|
||||
meta.inputs.action = function({core}) {
|
||||
//Build query object from inputs
|
||||
const q = {}
|
||||
for (const key of Object.keys(inputs)) {
|
||||
const value = `${core.getInput(key)}`.trim()
|
||||
//Inputs checks
|
||||
const result = Object.fromEntries(
|
||||
Object.entries(inputs).map(([key, {type, format, default:defaulted, min, max, values}]) => [
|
||||
//Format key
|
||||
metadata.to.query(key, {name}),
|
||||
//Format value
|
||||
(defaulted => {
|
||||
//Default value
|
||||
let value = q[metadata.to.query(key)] ?? q[key] ?? defaulted
|
||||
//Apply type conversion
|
||||
switch (type) {
|
||||
//Booleans
|
||||
case "boolean": {
|
||||
if (/^(?:[Tt]rue|[Oo]n|[Yy]es|1)$/.test(value))
|
||||
return true
|
||||
if (/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(value))
|
||||
return false
|
||||
return defaulted
|
||||
}
|
||||
//Numbers
|
||||
case "number": {
|
||||
value = Number(value)
|
||||
if (!Number.isFinite(value))
|
||||
value = defaulted
|
||||
if (Number.isFinite(min))
|
||||
value = Math.max(min, value)
|
||||
if (Number.isFinite(max))
|
||||
value = Math.min(value, max)
|
||||
return value
|
||||
}
|
||||
//Array
|
||||
case "array": {
|
||||
try {
|
||||
q[key] = decodeURIComponent(value)
|
||||
value = decodeURIComponent(value)
|
||||
}
|
||||
catch {
|
||||
logger(`metrics/inputs > failed to decode uri : ${value}`)
|
||||
q[key] = value
|
||||
value = defaulted
|
||||
}
|
||||
const separators = {"comma-separated":",", "space-separated":" "}
|
||||
const separator = separators[[format].flat().filter(s => s in separators)[0]] ?? ","
|
||||
return value.split(separator).map(v => v.trim().toLocaleLowerCase()).filter(v => Array.isArray(values) ? values.includes(v) : true).filter(v => v)
|
||||
}
|
||||
return meta.inputs({q, account:"bypass"})
|
||||
}
|
||||
}
|
||||
|
||||
//Web metadata
|
||||
{
|
||||
meta.web = Object.fromEntries(Object.entries(inputs).map(([key, {type, description:text, example, default:defaulted, min = 0, max = 9999, values}]) => [
|
||||
//Format key
|
||||
metadata.to.query(key),
|
||||
//Value descriptor
|
||||
(() => {
|
||||
switch (type) {
|
||||
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}
|
||||
case "number":
|
||||
return {text, type:"number", min, max, defaulted}
|
||||
case "array":
|
||||
return {text, type:"text", placeholder:example ?? defaulted, defaulted}
|
||||
case "string":{
|
||||
if (Array.isArray(values))
|
||||
return {text, type:"select", values, defaulted}
|
||||
return {text, type:"text", placeholder:example ?? defaulted, defaulted}
|
||||
//String
|
||||
case "string": {
|
||||
value = `${value}`.trim()
|
||||
if (user) {
|
||||
if (value === ".user.login")
|
||||
return user.login
|
||||
if (value === ".user.twitter")
|
||||
return user.twitterUsername
|
||||
if (value === ".user.website")
|
||||
return user.websiteUrl
|
||||
}
|
||||
case "json":
|
||||
return {text, type:"text", placeholder:example ?? defaulted, defaulted}
|
||||
default:
|
||||
return null
|
||||
if ((Array.isArray(values)) && (!values.includes(value)))
|
||||
return defaulted
|
||||
return value
|
||||
}
|
||||
})(),
|
||||
]).filter(([key, value]) => (value)&&(key !== name)))
|
||||
}
|
||||
|
||||
//Readme metadata
|
||||
{
|
||||
//Extract demos
|
||||
const raw = `${await fs.promises.readFile(path.join(__plugins, name, "README.md"), "utf-8")}`
|
||||
const demo = raw.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? "<td></td>"
|
||||
|
||||
//Readme descriptor
|
||||
meta.readme = {demo}
|
||||
}
|
||||
|
||||
//Icon
|
||||
meta.icon = meta.name.split(" ")[0] ?? null
|
||||
|
||||
//Result
|
||||
return meta
|
||||
//JSON
|
||||
case "json": {
|
||||
try {
|
||||
value = JSON.parse(value)
|
||||
}
|
||||
catch {
|
||||
logger(`metrics/inputs > failed to parse json : ${value}`)
|
||||
value = JSON.parse(defaulted)
|
||||
}
|
||||
return value
|
||||
}
|
||||
//Token
|
||||
case "token": {
|
||||
return value
|
||||
}
|
||||
//Default
|
||||
default: {
|
||||
return value
|
||||
}
|
||||
}
|
||||
})(defaults[key] ?? defaulted),
|
||||
]),
|
||||
)
|
||||
logger(`metrics/inputs > ${name} > ${JSON.stringify(result)}`)
|
||||
return result
|
||||
}
|
||||
Object.assign(meta.inputs, inputs, Object.fromEntries(Object.entries(inputs).map(([key, value]) => [metadata.to.query(key, {name}), value])))
|
||||
}
|
||||
catch (error) {
|
||||
logger(`metrics/metadata > failed to load plugin ${name}: ${error}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**Metadata extractor for templates */
|
||||
metadata.template = async function({__templates, name, plugins, logger}) {
|
||||
try {
|
||||
//Load meta descriptor
|
||||
const raw = fs.existsSync(path.join(__templates, name, "metadata.yml")) ? `${await fs.promises.readFile(path.join(__templates, name, "metadata.yml"), "utf-8")}` : ""
|
||||
const readme = `${await fs.promises.readFile(path.join(__templates, name, "README.md"), "utf-8")}`
|
||||
const meta = yaml.load(raw) ?? {}
|
||||
//Action metadata
|
||||
{
|
||||
//Extract comments
|
||||
const comments = {}
|
||||
raw.split(/(?:\r?\n){2,}/m)
|
||||
.map(x => x.trim()).filter(x => x)
|
||||
.map(x => x.split("\n").map(y => y.trim()).join("\n"))
|
||||
.map(x => {
|
||||
const input = x.match(new RegExp(`^\\s*(?<input>${Object.keys(inputs).join("|")}):`, "m"))?.groups?.input ?? null
|
||||
if (input)
|
||||
comments[input] = x.match(new RegExp(`(?<comment>[\\s\\S]*?)(?=(?:${Object.keys(inputs).sort((a, b) => b.length - a.length).join("|")}):)`))?.groups?.comment
|
||||
})
|
||||
|
||||
//Compatibility
|
||||
const partials = path.join(__templates, name, "partials")
|
||||
const compatibility = Object.fromEntries(Object.entries(plugins).map(([key]) => [key, false]))
|
||||
if ((fs.existsSync(partials))&&((await fs.promises.lstat(partials)).isDirectory())) {
|
||||
for (let plugin of await fs.promises.readdir(partials)) {
|
||||
plugin = plugin.match(/(?<plugin>^[\s\S]+(?=[.]ejs$))/)?.groups?.plugin ?? null
|
||||
if (plugin in compatibility)
|
||||
compatibility[plugin] = true
|
||||
//Action descriptor
|
||||
meta.action = Object.fromEntries(
|
||||
Object.entries(inputs).map(([key, value]) => [
|
||||
key,
|
||||
{
|
||||
comment:comments[key] ?? "",
|
||||
descriptor:yaml.dump({[key]:Object.fromEntries(Object.entries(value).filter(([key]) => ["description", "default", "required"].includes(key)))}, {quotingType:'"', noCompatMode:true}),
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
//Action inputs
|
||||
meta.inputs.action = function({core}) {
|
||||
//Build query object from inputs
|
||||
const q = {}
|
||||
for (const key of Object.keys(inputs)) {
|
||||
const value = `${core.getInput(key)}`.trim()
|
||||
try {
|
||||
q[key] = decodeURIComponent(value)
|
||||
}
|
||||
catch {
|
||||
logger(`metrics/inputs > failed to decode uri : ${value}`)
|
||||
q[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
//Result
|
||||
return {
|
||||
name:meta.name ?? readme.match(/^### (?<name>[\s\S]+?)\n/)?.groups?.name?.trim(),
|
||||
index:meta.index ?? null,
|
||||
formats:meta.formats ?? null,
|
||||
supports:meta.supports ?? null,
|
||||
readme:{
|
||||
demo:readme.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? '<td align="center" colspan="2">See <a href="/source/templates/community/README.md">documentation</a> 🌍</td>' : "<td></td>"),
|
||||
compatibility:{...compatibility, base:true},
|
||||
},
|
||||
check({q, account = "bypass", format = null}) {
|
||||
//Support check
|
||||
if (account !== "bypass") {
|
||||
const context = q.repo ? "repository" : account
|
||||
if ((Array.isArray(this.supports))&&(!this.supports.includes(context)))
|
||||
throw new Error(`not supported for: ${context}`)
|
||||
}
|
||||
//Format check
|
||||
if ((format)&&(Array.isArray(this.formats))&&(!this.formats.includes(format)))
|
||||
throw new Error(`not supported for: ${format}`)
|
||||
},
|
||||
}
|
||||
return meta.inputs({q, account:"bypass"})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
logger(`metrics/metadata > failed to load template ${name}: ${error}`)
|
||||
return null
|
||||
|
||||
//Web metadata
|
||||
{
|
||||
meta.web = Object.fromEntries(
|
||||
Object.entries(inputs).map(([key, {type, description:text, example, default:defaulted, min = 0, max = 9999, values}]) => [
|
||||
//Format key
|
||||
metadata.to.query(key),
|
||||
//Value descriptor
|
||||
(() => {
|
||||
switch (type) {
|
||||
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}
|
||||
case "number":
|
||||
return {text, type:"number", min, max, defaulted}
|
||||
case "array":
|
||||
return {text, type:"text", placeholder:example ?? defaulted, defaulted}
|
||||
case "string": {
|
||||
if (Array.isArray(values))
|
||||
return {text, type:"select", values, defaulted}
|
||||
return {text, type:"text", placeholder:example ?? defaulted, defaulted}
|
||||
}
|
||||
case "json":
|
||||
return {text, type:"text", placeholder:example ?? defaulted, defaulted}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
})(),
|
||||
]).filter(([key, value]) => (value) && (key !== name)),
|
||||
)
|
||||
}
|
||||
|
||||
//Readme metadata
|
||||
{
|
||||
//Extract demos
|
||||
const raw = `${await fs.promises.readFile(path.join(__plugins, name, "README.md"), "utf-8")}`
|
||||
const demo = raw.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? "<td></td>"
|
||||
|
||||
//Readme descriptor
|
||||
meta.readme = {demo}
|
||||
}
|
||||
|
||||
//Icon
|
||||
meta.icon = meta.name.split(" ")[0] ?? null
|
||||
|
||||
//Result
|
||||
return meta
|
||||
}
|
||||
catch (error) {
|
||||
logger(`metrics/metadata > failed to load plugin ${name}: ${error}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**Metadata extractor for templates */
|
||||
metadata.template = async function({__templates, name, plugins, logger}) {
|
||||
try {
|
||||
//Load meta descriptor
|
||||
const raw = fs.existsSync(path.join(__templates, name, "metadata.yml")) ? `${await fs.promises.readFile(path.join(__templates, name, "metadata.yml"), "utf-8")}` : ""
|
||||
const readme = `${await fs.promises.readFile(path.join(__templates, name, "README.md"), "utf-8")}`
|
||||
const meta = yaml.load(raw) ?? {}
|
||||
|
||||
//Compatibility
|
||||
const partials = path.join(__templates, name, "partials")
|
||||
const compatibility = Object.fromEntries(Object.entries(plugins).map(([key]) => [key, false]))
|
||||
if ((fs.existsSync(partials)) && ((await fs.promises.lstat(partials)).isDirectory())) {
|
||||
for (let plugin of await fs.promises.readdir(partials)) {
|
||||
plugin = plugin.match(/(?<plugin>^[\s\S]+(?=[.]ejs$))/)?.groups?.plugin ?? null
|
||||
if (plugin in compatibility)
|
||||
compatibility[plugin] = true
|
||||
}
|
||||
}
|
||||
|
||||
//Result
|
||||
return {
|
||||
name:meta.name ?? readme.match(/^### (?<name>[\s\S]+?)\n/)?.groups?.name?.trim(),
|
||||
index:meta.index ?? null,
|
||||
formats:meta.formats ?? null,
|
||||
supports:meta.supports ?? null,
|
||||
readme:{
|
||||
demo:readme.match(/(?<demo><table>[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? '<td align="center" colspan="2">See <a href="/source/templates/community/README.md">documentation</a> 🌍</td>' : "<td></td>"),
|
||||
compatibility:{...compatibility, base:true},
|
||||
},
|
||||
check({q, account = "bypass", format = null}) {
|
||||
//Support check
|
||||
if (account !== "bypass") {
|
||||
const context = q.repo ? "repository" : account
|
||||
if ((Array.isArray(this.supports)) && (!this.supports.includes(context)))
|
||||
throw new Error(`not supported for: ${context}`)
|
||||
}
|
||||
//Format check
|
||||
if ((format) && (Array.isArray(this.formats)) && (!this.formats.includes(format)))
|
||||
throw new Error(`not supported for: ${format}`)
|
||||
},
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
logger(`metrics/metadata > failed to load template ${name}: ${error}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**Metadata converters */
|
||||
metadata.to = {
|
||||
query(key, {name = null} = {}) {
|
||||
key = key.replace(/^plugin_/, "").replace(/_/g, ".")
|
||||
return name ? key.replace(new RegExp(`^(${name}.)`, "g"), "") : key
|
||||
},
|
||||
}
|
||||
metadata.to = {
|
||||
query(key, {name = null} = {}) {
|
||||
key = key.replace(/^plugin_/, "").replace(/_/g, ".")
|
||||
return name ? key.replace(new RegExp(`^(${name}.)`, "g"), "") : key
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,231 +1,238 @@
|
||||
//Imports
|
||||
import fs from "fs"
|
||||
import metadata from "./metadata.mjs"
|
||||
import path from "path"
|
||||
import processes from "child_process"
|
||||
import util from "util"
|
||||
import url from "url"
|
||||
import yaml from "js-yaml"
|
||||
import OctokitRest from "@octokit/rest"
|
||||
import OctokitRest from "@octokit/rest"
|
||||
import processes from "child_process"
|
||||
import fs from "fs"
|
||||
import yaml from "js-yaml"
|
||||
import path from "path"
|
||||
import url from "url"
|
||||
import util from "util"
|
||||
import metadata from "./metadata.mjs"
|
||||
|
||||
//Templates and plugins
|
||||
const Templates = {}
|
||||
const Plugins = {}
|
||||
const Templates = {}
|
||||
const Plugins = {}
|
||||
|
||||
/**Setup */
|
||||
export default async function({log = true, nosettings = false, community = {}} = {}) {
|
||||
|
||||
//Paths
|
||||
const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../../..")
|
||||
const __statics = path.join(__metrics, "source/app/web/statics")
|
||||
const __templates = path.join(__metrics, "source/templates")
|
||||
const __plugins = path.join(__metrics, "source/plugins")
|
||||
const __package = path.join(__metrics, "package.json")
|
||||
const __settings = path.join(__metrics, "settings.json")
|
||||
const __modules = path.join(__metrics, "node_modules")
|
||||
|
||||
//Init
|
||||
const logger = log ? console.debug : () => null
|
||||
logger("metrics/setup > setup")
|
||||
const conf = {
|
||||
authenticated:null,
|
||||
templates:{},
|
||||
queries:{},
|
||||
settings:{},
|
||||
metadata:{},
|
||||
paths:{
|
||||
statics:__statics,
|
||||
templates:__templates,
|
||||
node_modules:__modules,
|
||||
},
|
||||
}
|
||||
|
||||
//Load settings
|
||||
logger("metrics/setup > load settings.json")
|
||||
if (fs.existsSync(__settings)) {
|
||||
if (nosettings)
|
||||
logger("metrics/setup > load settings.json > skipped because no settings is enabled")
|
||||
else {
|
||||
conf.settings = JSON.parse(`${await fs.promises.readFile(__settings)}`)
|
||||
logger("metrics/setup > load settings.json > success")
|
||||
}
|
||||
}
|
||||
else
|
||||
logger("metrics/setup > load settings.json > (missing)")
|
||||
if (!conf.settings.templates)
|
||||
conf.settings.templates = {default:"classic", enabled:[]}
|
||||
if (!conf.settings.plugins)
|
||||
conf.settings.plugins = {}
|
||||
conf.settings.community = {...conf.settings.community, ...community}
|
||||
conf.settings.plugins.base = {parts:["header", "activity", "community", "repositories", "metadata"]}
|
||||
if (conf.settings.debug)
|
||||
logger(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256}))
|
||||
|
||||
//Load package settings
|
||||
logger("metrics/setup > load package.json")
|
||||
conf.package = JSON.parse(`${await fs.promises.readFile(__package)}`)
|
||||
logger("metrics/setup > load package.json > success")
|
||||
|
||||
//Load community templates
|
||||
if ((typeof conf.settings.community.templates === "string")&&(conf.settings.community.templates.length)) {
|
||||
logger("metrics/setup > parsing community templates list")
|
||||
conf.settings.community.templates = [...new Set([...decodeURIComponent(conf.settings.community.templates).split(",").map(v => v.trim().toLocaleLowerCase()).filter(v => v)])]
|
||||
}
|
||||
if ((Array.isArray(conf.settings.community.templates))&&(conf.settings.community.templates.length)) {
|
||||
//Clean remote repository
|
||||
logger(`metrics/setup > ${conf.settings.community.templates.length} community templates to install`)
|
||||
await fs.promises.rmdir(path.join(__templates, ".community"), {recursive:true})
|
||||
//Download community templates
|
||||
for (const template of conf.settings.community.templates) {
|
||||
try {
|
||||
//Parse community template
|
||||
logger(`metrics/setup > load community template ${template}`)
|
||||
const {repo, branch, name, trust = false} = template.match(/^(?<repo>[\s\S]+?)@(?<branch>[\s\S]+?):(?<name>[\s\S]+?)(?<trust>[+]trust)?$/)?.groups ?? null
|
||||
const command = `git clone --single-branch --branch ${branch} https://github.com/${repo}.git ${path.join(__templates, ".community")}`
|
||||
logger(`metrics/setup > run ${command}`)
|
||||
//Clone remote repository
|
||||
processes.execSync(command, {stdio:"ignore"})
|
||||
//Extract template
|
||||
logger(`metrics/setup > extract ${name} from ${repo}@${branch}`)
|
||||
await fs.promises.rmdir(path.join(__templates, `@${name}`), {recursive:true})
|
||||
await fs.promises.rename(path.join(__templates, ".community/source/templates", name), path.join(__templates, `@${name}`))
|
||||
//JavaScript file
|
||||
if (trust)
|
||||
logger(`metrics/setup > keeping @${name}/template.mjs (unsafe mode is enabled)`)
|
||||
else if (fs.existsSync(path.join(__templates, `@${name}`, "template.mjs"))) {
|
||||
logger(`metrics/setup > removing @${name}/template.mjs`)
|
||||
await fs.promises.unlink(path.join(__templates, `@${name}`, "template.mjs"))
|
||||
const inherit = yaml.load(`${fs.promises.readFile(path.join(__templates, `@${name}`, "metadata.yml"))}`).extends ?? null
|
||||
if (inherit) {
|
||||
logger(`metrics/setup > @${name} extends from ${inherit}`)
|
||||
if (fs.existsSync(path.join(__templates, inherit, "template.mjs"))) {
|
||||
logger(`metrics/setup > @${name} extended from ${inherit}`)
|
||||
await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs"))
|
||||
}
|
||||
else
|
||||
logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`)
|
||||
}
|
||||
}
|
||||
else
|
||||
logger(`metrics/setup > @${name}/template.mjs does not exist`)
|
||||
//Clean remote repository
|
||||
logger(`metrics/setup > clean ${repo}@${branch}`)
|
||||
await fs.promises.rmdir(path.join(__templates, ".community"), {recursive:true})
|
||||
logger(`metrics/setup > loaded community template ${name}`)
|
||||
}
|
||||
catch (error) {
|
||||
logger(`metrics/setup > failed to load community template ${template}`)
|
||||
logger(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
logger("metrics/setup > no community templates to install")
|
||||
|
||||
//Load templates
|
||||
for (const name of await fs.promises.readdir(__templates)) {
|
||||
//Search for templates
|
||||
const directory = path.join(__templates, name)
|
||||
if ((!(await fs.promises.lstat(directory)).isDirectory())||(!fs.existsSync(path.join(directory, "partials/_.json"))))
|
||||
continue
|
||||
logger(`metrics/setup > load template [${name}]`)
|
||||
//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 [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"))}`)
|
||||
conf.templates[name] = {image, style, fonts, partials, views:[directory]}
|
||||
|
||||
//Cache templates scripts
|
||||
Templates[name] = await (async() => {
|
||||
const template = path.join(directory, "template.mjs")
|
||||
const fallback = path.join(__templates, "classic", "template.mjs")
|
||||
return (await import(url.pathToFileURL(fs.existsSync(template) ? template : fallback).href)).default
|
||||
})()
|
||||
logger(`metrics/setup > load template [${name}] > success`)
|
||||
//Debug
|
||||
if (conf.settings.debug) {
|
||||
Object.defineProperty(conf.templates, name, {
|
||||
get() {
|
||||
logger(`metrics/setup > reload template [${name}]`)
|
||||
const [image, style, fonts] = files.map(file => `${fs.readFileSync(file)}`)
|
||||
const partials = JSON.parse(`${fs.readFileSync(path.join(directory, "partials/_.json"))}`)
|
||||
logger(`metrics/setup > reload template [${name}] > success`)
|
||||
return {image, style, fonts, partials, views:[directory]}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//Load plugins
|
||||
for (const name of await fs.promises.readdir(__plugins)) {
|
||||
//Search for plugins
|
||||
const directory = path.join(__plugins, name)
|
||||
if (!(await fs.promises.lstat(directory)).isDirectory())
|
||||
continue
|
||||
//Cache plugins scripts
|
||||
logger(`metrics/setup > load plugin [${name}]`)
|
||||
Plugins[name] = (await import(url.pathToFileURL(path.join(directory, "index.mjs")).href)).default
|
||||
logger(`metrics/setup > load plugin [${name}] > success`)
|
||||
//Register queries
|
||||
const __queries = path.join(directory, "queries")
|
||||
if (fs.existsSync(__queries)) {
|
||||
//Alias for default query
|
||||
const queries = function() {
|
||||
if (!queries[name])
|
||||
throw new ReferenceError(`Default query for ${name} undefined`)
|
||||
return queries[name](...arguments)
|
||||
}
|
||||
conf.queries[name] = queries
|
||||
//Load queries
|
||||
for (const file of await fs.promises.readdir(__queries)) {
|
||||
//Cache queries
|
||||
const query = file.replace(/[.]graphql$/, "")
|
||||
logger(`metrics/setup > load query [${name}/${query}]`)
|
||||
queries[`_${query}`] = `${await fs.promises.readFile(path.join(__queries, file))}`
|
||||
logger(`metrics/setup > load query [${name}/${query}] > success`)
|
||||
//Debug
|
||||
if (conf.settings.debug) {
|
||||
Object.defineProperty(queries, `_${query}`, {
|
||||
get() {
|
||||
logger(`metrics/setup > reload query [${name}/${query}]`)
|
||||
const raw = `${fs.readFileSync(path.join(__queries, file))}`
|
||||
logger(`metrics/setup > reload query [${name}/${query}] > success`)
|
||||
return raw
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
//Create queries formatters
|
||||
Object.keys(queries).map(query => queries[query.substring(1)] = (vars = {}) => {
|
||||
let queried = queries[query]
|
||||
for (const [key, value] of Object.entries(vars))
|
||||
queried = queried.replace(new RegExp(`[$]${key}`, "g"), value)
|
||||
return queried
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//Load metadata
|
||||
conf.metadata = await metadata({log})
|
||||
|
||||
//Store authenticated user
|
||||
if (conf.settings.token) {
|
||||
try {
|
||||
conf.authenticated = (await (new OctokitRest.Octokit({auth:conf.settings.token})).users.getAuthenticated()).data.login
|
||||
logger(`metrics/setup > setup > authenticated as ${conf.authenticated}`)
|
||||
}
|
||||
catch (error) {
|
||||
logger(`metrics/setup > setup > could not verify authentication : ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
//Set no token property
|
||||
Object.defineProperty(conf.settings, "notoken", {get() {
|
||||
return conf.settings.token === "NOT_NEEDED"
|
||||
}})
|
||||
|
||||
//Conf
|
||||
logger("metrics/setup > setup > success")
|
||||
return {Templates, Plugins, conf}
|
||||
export default async function({log = true, nosettings = false, community = {}} = {}) {
|
||||
//Paths
|
||||
const __metrics = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "../../..")
|
||||
const __statics = path.join(__metrics, "source/app/web/statics")
|
||||
const __templates = path.join(__metrics, "source/templates")
|
||||
const __plugins = path.join(__metrics, "source/plugins")
|
||||
const __package = path.join(__metrics, "package.json")
|
||||
const __settings = path.join(__metrics, "settings.json")
|
||||
const __modules = path.join(__metrics, "node_modules")
|
||||
|
||||
//Init
|
||||
const logger = log ? console.debug : () => null
|
||||
logger("metrics/setup > setup")
|
||||
const conf = {
|
||||
authenticated:null,
|
||||
templates:{},
|
||||
queries:{},
|
||||
settings:{},
|
||||
metadata:{},
|
||||
paths:{
|
||||
statics:__statics,
|
||||
templates:__templates,
|
||||
node_modules:__modules,
|
||||
},
|
||||
}
|
||||
|
||||
//Load settings
|
||||
logger("metrics/setup > load settings.json")
|
||||
if (fs.existsSync(__settings)) {
|
||||
if (nosettings)
|
||||
logger("metrics/setup > load settings.json > skipped because no settings is enabled")
|
||||
else {
|
||||
conf.settings = JSON.parse(`${await fs.promises.readFile(__settings)}`)
|
||||
logger("metrics/setup > load settings.json > success")
|
||||
}
|
||||
}
|
||||
else
|
||||
logger("metrics/setup > load settings.json > (missing)")
|
||||
|
||||
|
||||
if (!conf.settings.templates)
|
||||
conf.settings.templates = {default:"classic", enabled:[]}
|
||||
if (!conf.settings.plugins)
|
||||
conf.settings.plugins = {}
|
||||
conf.settings.community = {...conf.settings.community, ...community}
|
||||
conf.settings.plugins.base = {parts:["header", "activity", "community", "repositories", "metadata"]}
|
||||
if (conf.settings.debug)
|
||||
logger(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256}))
|
||||
|
||||
//Load package settings
|
||||
logger("metrics/setup > load package.json")
|
||||
conf.package = JSON.parse(`${await fs.promises.readFile(__package)}`)
|
||||
logger("metrics/setup > load package.json > success")
|
||||
|
||||
//Load community templates
|
||||
if ((typeof conf.settings.community.templates === "string") && (conf.settings.community.templates.length)) {
|
||||
logger("metrics/setup > parsing community templates list")
|
||||
conf.settings.community.templates = [...new Set([...decodeURIComponent(conf.settings.community.templates).split(",").map(v => v.trim().toLocaleLowerCase()).filter(v => v)])]
|
||||
}
|
||||
if ((Array.isArray(conf.settings.community.templates)) && (conf.settings.community.templates.length)) {
|
||||
//Clean remote repository
|
||||
logger(`metrics/setup > ${conf.settings.community.templates.length} community templates to install`)
|
||||
await fs.promises.rmdir(path.join(__templates, ".community"), {recursive:true})
|
||||
//Download community templates
|
||||
for (const template of conf.settings.community.templates) {
|
||||
try {
|
||||
//Parse community template
|
||||
logger(`metrics/setup > load community template ${template}`)
|
||||
const {repo, branch, name, trust = false} = template.match(/^(?<repo>[\s\S]+?)@(?<branch>[\s\S]+?):(?<name>[\s\S]+?)(?<trust>[+]trust)?$/)?.groups ?? null
|
||||
const command = `git clone --single-branch --branch ${branch} https://github.com/${repo}.git ${path.join(__templates, ".community")}`
|
||||
logger(`metrics/setup > run ${command}`)
|
||||
//Clone remote repository
|
||||
processes.execSync(command, {stdio:"ignore"})
|
||||
//Extract template
|
||||
logger(`metrics/setup > extract ${name} from ${repo}@${branch}`)
|
||||
await fs.promises.rmdir(path.join(__templates, `@${name}`), {recursive:true})
|
||||
await fs.promises.rename(path.join(__templates, ".community/source/templates", name), path.join(__templates, `@${name}`))
|
||||
//JavaScript file
|
||||
if (trust)
|
||||
logger(`metrics/setup > keeping @${name}/template.mjs (unsafe mode is enabled)`)
|
||||
else if (fs.existsSync(path.join(__templates, `@${name}`, "template.mjs"))) {
|
||||
logger(`metrics/setup > removing @${name}/template.mjs`)
|
||||
await fs.promises.unlink(path.join(__templates, `@${name}`, "template.mjs"))
|
||||
const inherit = yaml.load(`${fs.promises.readFile(path.join(__templates, `@${name}`, "metadata.yml"))}`).extends ?? null
|
||||
if (inherit) {
|
||||
logger(`metrics/setup > @${name} extends from ${inherit}`)
|
||||
if (fs.existsSync(path.join(__templates, inherit, "template.mjs"))) {
|
||||
logger(`metrics/setup > @${name} extended from ${inherit}`)
|
||||
await fs.promises.copyFile(path.join(__templates, inherit, "template.mjs"), path.join(__templates, `@${name}`, "template.mjs"))
|
||||
}
|
||||
else
|
||||
logger(`metrics/setup > @${name} could not extends ${inherit} as it does not exist`)
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
logger(`metrics/setup > @${name}/template.mjs does not exist`)
|
||||
|
||||
|
||||
//Clean remote repository
|
||||
logger(`metrics/setup > clean ${repo}@${branch}`)
|
||||
await fs.promises.rmdir(path.join(__templates, ".community"), {recursive:true})
|
||||
logger(`metrics/setup > loaded community template ${name}`)
|
||||
}
|
||||
catch (error) {
|
||||
logger(`metrics/setup > failed to load community template ${template}`)
|
||||
logger(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
logger("metrics/setup > no community templates to install")
|
||||
|
||||
|
||||
//Load templates
|
||||
for (const name of await fs.promises.readdir(__templates)) {
|
||||
//Search for templates
|
||||
const directory = path.join(__templates, name)
|
||||
if ((!(await fs.promises.lstat(directory)).isDirectory()) || (!fs.existsSync(path.join(directory, "partials/_.json"))))
|
||||
continue
|
||||
logger(`metrics/setup > load template [${name}]`)
|
||||
//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 [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"))}`)
|
||||
conf.templates[name] = {image, style, fonts, partials, views:[directory]}
|
||||
|
||||
//Cache templates scripts
|
||||
Templates[name] = await (async () => {
|
||||
const template = path.join(directory, "template.mjs")
|
||||
const fallback = path.join(__templates, "classic", "template.mjs")
|
||||
return (await import(url.pathToFileURL(fs.existsSync(template) ? template : fallback).href)).default
|
||||
})()
|
||||
logger(`metrics/setup > load template [${name}] > success`)
|
||||
//Debug
|
||||
if (conf.settings.debug) {
|
||||
Object.defineProperty(conf.templates, name, {
|
||||
get() {
|
||||
logger(`metrics/setup > reload template [${name}]`)
|
||||
const [image, style, fonts] = files.map(file => `${fs.readFileSync(file)}`)
|
||||
const partials = JSON.parse(`${fs.readFileSync(path.join(directory, "partials/_.json"))}`)
|
||||
logger(`metrics/setup > reload template [${name}] > success`)
|
||||
return {image, style, fonts, partials, views:[directory]}
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//Load plugins
|
||||
for (const name of await fs.promises.readdir(__plugins)) {
|
||||
//Search for plugins
|
||||
const directory = path.join(__plugins, name)
|
||||
if (!(await fs.promises.lstat(directory)).isDirectory())
|
||||
continue
|
||||
//Cache plugins scripts
|
||||
logger(`metrics/setup > load plugin [${name}]`)
|
||||
Plugins[name] = (await import(url.pathToFileURL(path.join(directory, "index.mjs")).href)).default
|
||||
logger(`metrics/setup > load plugin [${name}] > success`)
|
||||
//Register queries
|
||||
const __queries = path.join(directory, "queries")
|
||||
if (fs.existsSync(__queries)) {
|
||||
//Alias for default query
|
||||
const queries = function() {
|
||||
if (!queries[name])
|
||||
throw new ReferenceError(`Default query for ${name} undefined`)
|
||||
return queries[name](...arguments)
|
||||
}
|
||||
conf.queries[name] = queries
|
||||
//Load queries
|
||||
for (const file of await fs.promises.readdir(__queries)) {
|
||||
//Cache queries
|
||||
const query = file.replace(/[.]graphql$/, "")
|
||||
logger(`metrics/setup > load query [${name}/${query}]`)
|
||||
queries[`_${query}`] = `${await fs.promises.readFile(path.join(__queries, file))}`
|
||||
logger(`metrics/setup > load query [${name}/${query}] > success`)
|
||||
//Debug
|
||||
if (conf.settings.debug) {
|
||||
Object.defineProperty(queries, `_${query}`, {
|
||||
get() {
|
||||
logger(`metrics/setup > reload query [${name}/${query}]`)
|
||||
const raw = `${fs.readFileSync(path.join(__queries, file))}`
|
||||
logger(`metrics/setup > reload query [${name}/${query}] > success`)
|
||||
return raw
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
//Create queries formatters
|
||||
Object.keys(queries).map(query => queries[query.substring(1)] = (vars = {}) => {
|
||||
let queried = queries[query]
|
||||
for (const [key, value] of Object.entries(vars))
|
||||
queried = queried.replace(new RegExp(`[$]${key}`, "g"), value)
|
||||
return queried
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
//Load metadata
|
||||
conf.metadata = await metadata({log})
|
||||
|
||||
//Store authenticated user
|
||||
if (conf.settings.token) {
|
||||
try {
|
||||
conf.authenticated = (await (new OctokitRest.Octokit({auth:conf.settings.token})).users.getAuthenticated()).data.login
|
||||
logger(`metrics/setup > setup > authenticated as ${conf.authenticated}`)
|
||||
}
|
||||
catch (error) {
|
||||
logger(`metrics/setup > setup > could not verify authentication : ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
//Set no token property
|
||||
Object.defineProperty(conf.settings, "notoken", {
|
||||
get() {
|
||||
return conf.settings.token === "NOT_NEEDED"
|
||||
},
|
||||
})
|
||||
|
||||
//Conf
|
||||
logger("metrics/setup > setup > success")
|
||||
return {Templates, Plugins, conf}
|
||||
}
|
||||
|
||||
@@ -1,422 +1,429 @@
|
||||
//Imports
|
||||
import fs from "fs/promises"
|
||||
import fss from "fs"
|
||||
import os from "os"
|
||||
import paths from "path"
|
||||
import url from "url"
|
||||
import util from "util"
|
||||
import processes from "child_process"
|
||||
import axios from "axios"
|
||||
import _puppeteer from "puppeteer"
|
||||
import git from "simple-git"
|
||||
import twemojis from "twemoji-parser"
|
||||
import jimp from "jimp"
|
||||
import opengraph from "open-graph-scraper"
|
||||
import rss from "rss-parser"
|
||||
import nodechartist from "node-chartist"
|
||||
import GIFEncoder from "gifencoder"
|
||||
import PNG from "png-js"
|
||||
import marked from "marked"
|
||||
import htmlsanitize from "sanitize-html"
|
||||
import prism from "prismjs"
|
||||
import prism_lang from "prismjs/components/index.js"
|
||||
prism_lang()
|
||||
import fs from "fs/promises"
|
||||
import prism_lang from "prismjs/components/index.js"
|
||||
import axios from "axios"
|
||||
import processes from "child_process"
|
||||
import fss from "fs"
|
||||
import GIFEncoder from "gifencoder"
|
||||
import jimp from "jimp"
|
||||
import marked from "marked"
|
||||
import nodechartist from "node-chartist"
|
||||
import opengraph from "open-graph-scraper"
|
||||
import os from "os"
|
||||
import paths from "path"
|
||||
import PNG from "png-js"
|
||||
import prism from "prismjs"
|
||||
import _puppeteer from "puppeteer"
|
||||
import rss from "rss-parser"
|
||||
import htmlsanitize from "sanitize-html"
|
||||
import git from "simple-git"
|
||||
import twemojis from "twemoji-parser"
|
||||
import url from "url"
|
||||
import util from "util"
|
||||
prism_lang()
|
||||
|
||||
//Exports
|
||||
export {fs, os, paths, url, util, processes, axios, git, opengraph, jimp, rss}
|
||||
export {axios, fs, git, jimp, opengraph, os, paths, processes, rss, url, util}
|
||||
|
||||
/**Returns module __dirname */
|
||||
export function __module(module) {
|
||||
return paths.join(paths.dirname(url.fileURLToPath(module)))
|
||||
}
|
||||
export function __module(module) {
|
||||
return paths.join(paths.dirname(url.fileURLToPath(module)))
|
||||
}
|
||||
|
||||
/**Puppeteer instantier */
|
||||
export const puppeteer = {
|
||||
async launch() {
|
||||
return _puppeteer.launch({
|
||||
headless:this.headless,
|
||||
executablePath:process.env.PUPPETEER_BROWSER_PATH,
|
||||
args:this.headless ? ["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] : [],
|
||||
ignoreDefaultArgs:["--disable-extensions"],
|
||||
})
|
||||
},
|
||||
headless:true,
|
||||
}
|
||||
export const puppeteer = {
|
||||
async launch() {
|
||||
return _puppeteer.launch({
|
||||
headless:this.headless,
|
||||
executablePath:process.env.PUPPETEER_BROWSER_PATH,
|
||||
args:this.headless ? ["--no-sandbox", "--disable-extensions", "--disable-setuid-sandbox", "--disable-dev-shm-usage"] : [],
|
||||
ignoreDefaultArgs:["--disable-extensions"],
|
||||
})
|
||||
},
|
||||
headless:true,
|
||||
}
|
||||
|
||||
/**Plural formatter */
|
||||
export function s(value, end = "") {
|
||||
return value !== 1 ? {y:"ies", "":"s"}[end] : end
|
||||
}
|
||||
export function s(value, end = "") {
|
||||
return value !== 1 ? {y:"ies", "":"s"}[end] : end
|
||||
}
|
||||
|
||||
/**Formatter */
|
||||
export function format(n, {sign = false, unit = true, fixed} = {}) {
|
||||
if (unit) {
|
||||
for (const {u, v} of [{u:"b", v:10**9}, {u:"m", v:10**6}, {u:"k", v:10**3}]) {
|
||||
if (n/v >= 1)
|
||||
return `${(sign)&&(n > 0) ? "+" : ""}${(n/v).toFixed(fixed ?? 2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
|
||||
}
|
||||
export function format(n, {sign = false, unit = true, fixed} = {}) {
|
||||
if (unit) {
|
||||
for (const {u, v} of [{u:"b", v:10 ** 9}, {u:"m", v:10 ** 6}, {u:"k", v:10 ** 3}]) {
|
||||
if (n / v >= 1)
|
||||
return `${(sign) && (n > 0) ? "+" : ""}${(n / v).toFixed(fixed ?? 2).substr(0, 4).replace(/[.]0*$/, "")}${u}`
|
||||
}
|
||||
return `${(sign)&&(n > 0) ? "+" : ""}${fixed ? n.toFixed(fixed) : n}`
|
||||
}
|
||||
return `${(sign) && (n > 0) ? "+" : ""}${fixed ? n.toFixed(fixed) : n}`
|
||||
}
|
||||
|
||||
/**Bytes formatter */
|
||||
export function bytes(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}]) {
|
||||
if (n/v >= 1)
|
||||
return `${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B`
|
||||
}
|
||||
return `${n} byte${n > 1 ? "s" : ""}`
|
||||
export function bytes(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}]) {
|
||||
if (n / v >= 1)
|
||||
return `${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B`
|
||||
}
|
||||
format.bytes = bytes
|
||||
return `${n} byte${n > 1 ? "s" : ""}`
|
||||
}
|
||||
format.bytes = bytes
|
||||
|
||||
/**Percentage formatter */
|
||||
export function percentage(n, {rescale = true} = {}) {
|
||||
return `${(n*(rescale ? 100 : 1)).toFixed(2)
|
||||
export function percentage(n, {rescale = true} = {}) {
|
||||
return `${
|
||||
(n * (rescale ? 100 : 1)).toFixed(2)
|
||||
.replace(/(?<=[.])(?<decimal>[1-9]*)0+$/, "$<decimal>")
|
||||
.replace(/[.]$/, "")}%`
|
||||
}
|
||||
format.percentage = percentage
|
||||
.replace(/[.]$/, "")
|
||||
}%`
|
||||
}
|
||||
format.percentage = percentage
|
||||
|
||||
/**Text ellipsis formatter */
|
||||
export function ellipsis(text, {length = 20} = {}) {
|
||||
text = `${text}`
|
||||
if (text.length < length)
|
||||
return text
|
||||
return `${text.substring(0, length)}…`
|
||||
}
|
||||
format.ellipsis = ellipsis
|
||||
export function ellipsis(text, {length = 20} = {}) {
|
||||
text = `${text}`
|
||||
if (text.length < length)
|
||||
return text
|
||||
return `${text.substring(0, length)}…`
|
||||
}
|
||||
format.ellipsis = ellipsis
|
||||
|
||||
/**Date formatter */
|
||||
export function date(string, options) {
|
||||
return new Intl.DateTimeFormat("en-GB", options).format(new Date(string))
|
||||
}
|
||||
format.date = date
|
||||
export function date(string, options) {
|
||||
return new Intl.DateTimeFormat("en-GB", options).format(new Date(string))
|
||||
}
|
||||
format.date = date
|
||||
|
||||
/**Array shuffler */
|
||||
export function shuffle(array) {
|
||||
for (let i = array.length-1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random()*(i+1))
|
||||
;[array[i], array[j]] = [array[j], array[i]]
|
||||
}
|
||||
return array
|
||||
export function shuffle(array) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1))
|
||||
;[array[i], array[j]] = [array[j], array[i]]
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
/**Escape html */
|
||||
export function htmlescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) {
|
||||
return string
|
||||
.replace(/&(?!(?:amp|lt|gt|quot|apos);)/g, u["&"] ? "&" : "&")
|
||||
.replace(/</g, u["<"] ? "<" : "<")
|
||||
.replace(/>/g, u[">"] ? ">" : ">")
|
||||
.replace(/"/g, u['"'] ? """ : '"')
|
||||
.replace(/'/g, u["'"] ? "'" : "'")
|
||||
}
|
||||
export function htmlescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) {
|
||||
return string
|
||||
.replace(/&(?!(?:amp|lt|gt|quot|apos);)/g, u["&"] ? "&" : "&")
|
||||
.replace(/</g, u["<"] ? "<" : "<")
|
||||
.replace(/>/g, u[">"] ? ">" : ">")
|
||||
.replace(/"/g, u['"'] ? """ : '"')
|
||||
.replace(/'/g, u["'"] ? "'" : "'")
|
||||
}
|
||||
|
||||
/**Unescape html */
|
||||
export function htmlunescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) {
|
||||
return string
|
||||
.replace(/</g, u["<"] ? "<" : "<")
|
||||
.replace(/>/g, u[">"] ? ">" : ">")
|
||||
.replace(/"/g, u['"'] ? '"' : """)
|
||||
.replace(/&(?:apos|#39);/g, u["'"] ? "'" : "'")
|
||||
.replace(/&/g, u["&"] ? "&" : "&")
|
||||
}
|
||||
export function htmlunescape(string, u = {"&":true, "<":true, ">":true, '"':true, "'":true}) {
|
||||
return string
|
||||
.replace(/</g, u["<"] ? "<" : "<")
|
||||
.replace(/>/g, u[">"] ? ">" : ">")
|
||||
.replace(/"/g, u['"'] ? '"' : """)
|
||||
.replace(/&(?:apos|#39);/g, u["'"] ? "'" : "'")
|
||||
.replace(/&/g, u["&"] ? "&" : "&")
|
||||
}
|
||||
|
||||
/**Chartist */
|
||||
export async function chartist() {
|
||||
const css = `<style>${await fs.readFile(paths.join(__module(import.meta.url), "../../../node_modules", "node-chartist/dist/main.css")).catch(_ => "")}</style>`
|
||||
return (await nodechartist(...arguments))
|
||||
.replace(/class="ct-chart-line">/, `class="ct-chart-line">${css}`)
|
||||
}
|
||||
export async function chartist() {
|
||||
const css = `<style>${await fs.readFile(paths.join(__module(import.meta.url), "../../../node_modules", "node-chartist/dist/main.css")).catch(_ => "")}</style>`
|
||||
return (await nodechartist(...arguments))
|
||||
.replace(/class="ct-chart-line">/, `class="ct-chart-line">${css}`)
|
||||
}
|
||||
|
||||
/**Run command */
|
||||
export async function run(command, options, {prefixed = true} = {}) {
|
||||
const prefix = {win32:"wsl"}[process.platform] ?? ""
|
||||
command = `${prefixed ? prefix : ""} ${command}`.trim()
|
||||
return new Promise((solve, reject) => {
|
||||
console.debug(`metrics/command > ${command}`)
|
||||
const child = processes.exec(command, options)
|
||||
let [stdout, stderr] = ["", ""]
|
||||
child.stdout.on("data", data => stdout += data)
|
||||
child.stderr.on("data", data => stderr += data)
|
||||
child.on("close", code => {
|
||||
console.debug(`metrics/command > ${command} > exited with code ${code}`)
|
||||
console.debug(stdout)
|
||||
console.debug(stderr)
|
||||
return code === 0 ? solve(stdout) : reject(stderr)
|
||||
})
|
||||
export async function run(command, options, {prefixed = true} = {}) {
|
||||
const prefix = {win32:"wsl"}[process.platform] ?? ""
|
||||
command = `${prefixed ? prefix : ""} ${command}`.trim()
|
||||
return new Promise((solve, reject) => {
|
||||
console.debug(`metrics/command > ${command}`)
|
||||
const child = processes.exec(command, options)
|
||||
let [stdout, stderr] = ["", ""]
|
||||
child.stdout.on("data", data => stdout += data)
|
||||
child.stderr.on("data", data => stderr += data)
|
||||
child.on("close", code => {
|
||||
console.debug(`metrics/command > ${command} > exited with code ${code}`)
|
||||
console.debug(stdout)
|
||||
console.debug(stderr)
|
||||
return code === 0 ? solve(stdout) : reject(stderr)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**Check command existance */
|
||||
export async function which(command) {
|
||||
try {
|
||||
console.debug(`metrics/command > checking existence of ${command}`)
|
||||
await run(`which ${command}`)
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
console.debug(`metrics/command > checking existence of ${command} > failed`)
|
||||
}
|
||||
return false
|
||||
export async function which(command) {
|
||||
try {
|
||||
console.debug(`metrics/command > checking existence of ${command}`)
|
||||
await run(`which ${command}`)
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
console.debug(`metrics/command > checking existence of ${command} > failed`)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**Markdown-html sanitizer-interpreter */
|
||||
export async function markdown(text, {mode = "inline", codelines = Infinity} = {}) {
|
||||
//Sanitize user input once to prevent injections and parse into markdown
|
||||
let rendered = await marked(htmlunescape(htmlsanitize(text)), {
|
||||
highlight(code, lang) {
|
||||
return lang in prism.languages ? prism.highlight(code, prism.languages[lang]) : code
|
||||
export async function markdown(text, {mode = "inline", codelines = Infinity} = {}) {
|
||||
//Sanitize user input once to prevent injections and parse into markdown
|
||||
let rendered = await marked(htmlunescape(htmlsanitize(text)), {
|
||||
highlight(code, lang) {
|
||||
return lang in prism.languages ? prism.highlight(code, prism.languages[lang]) : code
|
||||
},
|
||||
silent:true,
|
||||
xhtml:true,
|
||||
})
|
||||
//Markdown mode
|
||||
switch (mode) {
|
||||
case "inline": {
|
||||
rendered = htmlsanitize(
|
||||
htmlsanitize(rendered, {
|
||||
allowedTags:["h1", "h2", "h3", "h4", "h5", "h6", "br", "blockquote", "code", "span"],
|
||||
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"},
|
||||
},
|
||||
silent:true,
|
||||
xhtml:true,
|
||||
})
|
||||
//Markdown mode
|
||||
switch (mode) {
|
||||
case "inline":{
|
||||
rendered = htmlsanitize(htmlsanitize(rendered, {
|
||||
allowedTags:["h1", "h2", "h3", "h4", "h5", "h6", "br", "blockquote", "code", "span"],
|
||||
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"},
|
||||
})
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
//Trim code snippets
|
||||
rendered = rendered.replace(/(?<open><code[\s\S]*?>)(?<code>[\s\S]*?)(?<close><\/code>)/g, (m, open, code, close) => { //eslint-disable-line max-params
|
||||
const lines = code.trim().split("\n")
|
||||
if ((lines.length > 1)&&(!/class="[\s\S]*"/.test(open)))
|
||||
open = open.replace(/>/g, ' class="language-multiline">')
|
||||
return `${open}${lines.slice(0, codelines).join("\n")}${lines.length > codelines ? `\n<span class="token trimmed">(${lines.length-codelines} more ${lines.length-codelines === 1 ? "line was" : "lines were"} trimmed)</span>` : ""}${close}`
|
||||
})
|
||||
return rendered
|
||||
)
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
//Trim code snippets
|
||||
rendered = rendered.replace(/(?<open><code[\s\S]*?>)(?<code>[\s\S]*?)(?<close><\/code>)/g, (m, open, code, close) => { //eslint-disable-line max-params
|
||||
const lines = code.trim().split("\n")
|
||||
if ((lines.length > 1) && (!/class="[\s\S]*"/.test(open)))
|
||||
open = open.replace(/>/g, ' class="language-multiline">')
|
||||
return `${open}${lines.slice(0, codelines).join("\n")}${lines.length > codelines ? `\n<span class="token trimmed">(${lines.length - codelines} more ${lines.length - codelines === 1 ? "line was" : "lines were"} trimmed)</span>` : ""}${close}`
|
||||
})
|
||||
return rendered
|
||||
}
|
||||
|
||||
/**Check GitHub filter against object */
|
||||
export function ghfilter(text, object) {
|
||||
console.debug(`metrics/svg/ghquery > checking ${text} against ${JSON.stringify(object)}`)
|
||||
const result = text.split(" ").map(x => x.trim()).filter(x => x).map(criteria => {
|
||||
const [key, filters] = criteria.split(":")
|
||||
const value = object[key]
|
||||
console.debug(`metrics/svg/ghquery > checking ${criteria} against ${value}`)
|
||||
return filters.split(",").map(x => x.trim()).filter(x => x).map(filter => {
|
||||
switch (true) {
|
||||
case /^>\d+$/.test(filter):
|
||||
return value > Number(filter.substring(1))
|
||||
case /^<\d+$/.test(filter):
|
||||
return value < Number(filter.substring(1))
|
||||
case /^\d+$/.test(filter):
|
||||
return value === Number(filter)
|
||||
case /^\d+..\d+$/.test(filter):{
|
||||
const [a, b] = filter.split("..").map(Number)
|
||||
return (value >= a)&&(value <= b)
|
||||
}
|
||||
default:
|
||||
return false
|
||||
export function ghfilter(text, object) {
|
||||
console.debug(`metrics/svg/ghquery > checking ${text} against ${JSON.stringify(object)}`)
|
||||
const result = text.split(" ").map(x => x.trim()).filter(x => x).map(criteria => {
|
||||
const [key, filters] = criteria.split(":")
|
||||
const value = object[key]
|
||||
console.debug(`metrics/svg/ghquery > checking ${criteria} against ${value}`)
|
||||
return filters.split(",").map(x => x.trim()).filter(x => x).map(filter => {
|
||||
switch (true) {
|
||||
case /^>\d+$/.test(filter):
|
||||
return value > Number(filter.substring(1))
|
||||
case /^<\d+$/.test(filter):
|
||||
return value < Number(filter.substring(1))
|
||||
case /^\d+$/.test(filter):
|
||||
return value === Number(filter)
|
||||
case /^\d+..\d+$/.test(filter): {
|
||||
const [a, b] = filter.split("..").map(Number)
|
||||
return (value >= a) && (value <= b)
|
||||
}
|
||||
}).reduce((a, b) => a||b, false)
|
||||
}).reduce((a, b) => a&&b, true)
|
||||
console.debug(`metrics/svg/ghquery > ${result ? "matching" : "not matching"}`)
|
||||
return result
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}).reduce((a, b) => a || b, false)
|
||||
}).reduce((a, b) => a && b, true)
|
||||
console.debug(`metrics/svg/ghquery > ${result ? "matching" : "not matching"}`)
|
||||
return result
|
||||
}
|
||||
|
||||
/**Image to base64 */
|
||||
export async function imgb64(image, {width, height, fallback = true} = {}) {
|
||||
//Undefined image
|
||||
if (!image)
|
||||
return fallback ? "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" : null
|
||||
//Load image
|
||||
image = await jimp.read(image)
|
||||
//Resize image
|
||||
if ((width)&&(height))
|
||||
image = image.resize(width, height)
|
||||
return image.getBase64Async(jimp.AUTO)
|
||||
}
|
||||
export async function imgb64(image, {width, height, fallback = true} = {}) {
|
||||
//Undefined image
|
||||
if (!image)
|
||||
return fallback ? "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" : null
|
||||
//Load image
|
||||
image = await jimp.read(image)
|
||||
//Resize image
|
||||
if ((width) && (height))
|
||||
image = image.resize(width, height)
|
||||
return image.getBase64Async(jimp.AUTO)
|
||||
}
|
||||
|
||||
/**SVG utils */
|
||||
export const svg = {
|
||||
/**Render as pdf */
|
||||
async pdf(rendered, {paddings = "", style = "", twemojis = false, gemojis = false, rest = null} = {}) {
|
||||
//Instantiate browser if needed
|
||||
if (!svg.resize.browser) {
|
||||
svg.resize.browser = await puppeteer.launch()
|
||||
console.debug(`metrics/svg/pdf > started ${await svg.resize.browser.version()}`)
|
||||
}
|
||||
//Additional transformations
|
||||
if (twemojis)
|
||||
rendered = await svg.twemojis(rendered, {custom:false})
|
||||
if ((gemojis)&&(rest))
|
||||
rendered = await svg.gemojis(rendered, {rest})
|
||||
rendered = marked(rendered)
|
||||
//Render through browser and print pdf
|
||||
console.debug("metrics/svg/pdf > loading svg")
|
||||
const page = await svg.resize.browser.newPage()
|
||||
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"]})
|
||||
console.debug("metrics/svg/pdf > loaded svg successfully")
|
||||
await page.addStyleTag({content:`
|
||||
export const svg = {
|
||||
/**Render as pdf */
|
||||
async pdf(rendered, {paddings = "", style = "", twemojis = false, gemojis = false, rest = null} = {}) {
|
||||
//Instantiate browser if needed
|
||||
if (!svg.resize.browser) {
|
||||
svg.resize.browser = await puppeteer.launch()
|
||||
console.debug(`metrics/svg/pdf > started ${await svg.resize.browser.version()}`)
|
||||
}
|
||||
//Additional transformations
|
||||
if (twemojis)
|
||||
rendered = await svg.twemojis(rendered, {custom:false})
|
||||
if ((gemojis) && (rest))
|
||||
rendered = await svg.gemojis(rendered, {rest})
|
||||
rendered = marked(rendered)
|
||||
//Render through browser and print pdf
|
||||
console.debug("metrics/svg/pdf > loading svg")
|
||||
const page = await svg.resize.browser.newPage()
|
||||
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"]})
|
||||
console.debug("metrics/svg/pdf > loaded svg successfully")
|
||||
await page.addStyleTag({
|
||||
content:`
|
||||
main { margin: ${(Array.isArray(paddings) ? paddings : paddings.split(",")).join(" ")}; }
|
||||
main svg { height: 1em; width: 1em; }
|
||||
${await fs.readFile(paths.join(__module(import.meta.url), "../../../node_modules", "@primer/css/dist/markdown.css")).catch(_ => "")}${style}
|
||||
`})
|
||||
rendered = await page.pdf()
|
||||
`,
|
||||
})
|
||||
rendered = await page.pdf()
|
||||
//Result
|
||||
await page.close()
|
||||
console.debug("metrics/svg/pdf > rendering complete")
|
||||
return {rendered, mime:"application/pdf"}
|
||||
},
|
||||
/**Render and resize svg */
|
||||
async resize(rendered, {paddings, convert}) {
|
||||
//Instantiate browser if needed
|
||||
if (!svg.resize.browser) {
|
||||
svg.resize.browser = await puppeteer.launch()
|
||||
console.debug(`metrics/svg/resize > started ${await svg.resize.browser.version()}`)
|
||||
}
|
||||
//Format padding
|
||||
const [pw = 1, ph] = (Array.isArray(paddings) ? paddings : `${paddings}`.split(",").map(x => x.trim())).map(padding => `${padding}`.substring(0, padding.length - 1)).map(value => 1 + Number(value) / 100)
|
||||
const padding = {width:pw, height:(ph ?? pw)}
|
||||
if (!Number.isFinite(padding.width))
|
||||
padding.width = 1
|
||||
if (!Number.isFinite(padding.height))
|
||||
padding.height = 1
|
||||
console.debug(`metrics/svg/resize > padding width*${padding.width}, height*${padding.height}`)
|
||||
//Render through browser and resize height
|
||||
console.debug("metrics/svg/resize > loading svg")
|
||||
const page = await svg.resize.browser.newPage()
|
||||
page.on("console", ({_text:text}) => console.debug(`metrics/svg/resize > puppeteer > ${text}`))
|
||||
await page.setContent(rendered, {waitUntil:["load", "domcontentloaded", "networkidle2"]})
|
||||
console.debug("metrics/svg/resize > loaded svg successfully")
|
||||
await page.addStyleTag({content:"body { margin: 0; padding: 0; }"})
|
||||
let mime = "image/svg+xml"
|
||||
console.debug("metrics/svg/resize > resizing svg")
|
||||
let height, resized, width
|
||||
try {
|
||||
({resized, width, height} = await page.evaluate(async padding => {
|
||||
//Disable animations
|
||||
const animated = !document.querySelector("svg").classList.contains("no-animations")
|
||||
if (animated)
|
||||
document.querySelector("svg").classList.add("no-animations")
|
||||
console.debug(`animations are ${animated ? "enabled" : "disabled"}`)
|
||||
await new Promise(solve => setTimeout(solve, 2400))
|
||||
//Get bounds and resize
|
||||
let {y:height, width} = document.querySelector("svg #metrics-end").getBoundingClientRect()
|
||||
console.debug(`bounds width=${width}, height=${height}`)
|
||||
height = Math.ceil(height * padding.height)
|
||||
width = Math.ceil(width * padding.width)
|
||||
console.debug(`bounds after applying padding width=${width} (*${padding.width}), height=${height} (*${padding.height})`)
|
||||
//Resize svg
|
||||
document.querySelector("svg").setAttribute("height", height)
|
||||
//Enable animations
|
||||
if (animated)
|
||||
document.querySelector("svg").classList.remove("no-animations")
|
||||
//Result
|
||||
await page.close()
|
||||
console.debug("metrics/svg/pdf > rendering complete")
|
||||
return {rendered, mime:"application/pdf"}
|
||||
},
|
||||
/**Render and resize svg */
|
||||
async resize(rendered, {paddings, convert}) {
|
||||
//Instantiate browser if needed
|
||||
if (!svg.resize.browser) {
|
||||
svg.resize.browser = await puppeteer.launch()
|
||||
console.debug(`metrics/svg/resize > started ${await svg.resize.browser.version()}`)
|
||||
}
|
||||
//Format padding
|
||||
const [pw = 1, ph] = (Array.isArray(paddings) ? paddings : `${paddings}`.split(",").map(x => x.trim())).map(padding => `${padding}`.substring(0, padding.length-1)).map(value => 1+Number(value)/100)
|
||||
const padding = {width:pw, height:(ph ?? pw)}
|
||||
if (!Number.isFinite(padding.width))
|
||||
padding.width = 1
|
||||
if (!Number.isFinite(padding.height))
|
||||
padding.height = 1
|
||||
console.debug(`metrics/svg/resize > padding width*${padding.width}, height*${padding.height}`)
|
||||
//Render through browser and resize height
|
||||
console.debug("metrics/svg/resize > loading svg")
|
||||
const page = await svg.resize.browser.newPage()
|
||||
page.on("console", ({_text:text}) => console.debug(`metrics/svg/resize > puppeteer > ${text}`))
|
||||
await page.setContent(rendered, {waitUntil:["load", "domcontentloaded", "networkidle2"]})
|
||||
console.debug("metrics/svg/resize > loaded svg successfully")
|
||||
await page.addStyleTag({content:"body { margin: 0; padding: 0; }"})
|
||||
let mime = "image/svg+xml"
|
||||
console.debug("metrics/svg/resize > resizing svg")
|
||||
let height, resized, width
|
||||
try {
|
||||
({resized, width, height} = await page.evaluate(async padding => {
|
||||
//Disable animations
|
||||
const animated = !document.querySelector("svg").classList.contains("no-animations")
|
||||
if (animated)
|
||||
document.querySelector("svg").classList.add("no-animations")
|
||||
console.debug(`animations are ${animated ? "enabled" : "disabled"}`)
|
||||
await new Promise(solve => setTimeout(solve, 2400))
|
||||
//Get bounds and resize
|
||||
let {y:height, width} = document.querySelector("svg #metrics-end").getBoundingClientRect()
|
||||
console.debug(`bounds width=${width}, height=${height}`)
|
||||
height = Math.ceil(height*padding.height)
|
||||
width = Math.ceil(width*padding.width)
|
||||
console.debug(`bounds after applying padding width=${width} (*${padding.width}), height=${height} (*${padding.height})`)
|
||||
//Resize svg
|
||||
document.querySelector("svg").setAttribute("height", height)
|
||||
//Enable animations
|
||||
if (animated)
|
||||
document.querySelector("svg").classList.remove("no-animations")
|
||||
//Result
|
||||
return {resized:new XMLSerializer().serializeToString(document.querySelector("svg")), height, width}
|
||||
}, padding))
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
console.debug(`metrics/svg/resize > an error occured: ${error}`)
|
||||
throw error
|
||||
}
|
||||
//Convert if required
|
||||
if (convert) {
|
||||
console.debug(`metrics/svg/resize > convert to ${convert}`)
|
||||
resized = await page.screenshot({type:convert, clip:{x:0, y:0, width, height}, omitBackground:true})
|
||||
mime = `image/${convert}`
|
||||
}
|
||||
//Result
|
||||
await page.close()
|
||||
console.debug("metrics/svg/resize > rendering complete")
|
||||
return {resized, mime}
|
||||
},
|
||||
/**Render twemojis */
|
||||
async twemojis(rendered, {custom = true} = {}) {
|
||||
//Load emojis
|
||||
console.debug("metrics/svg/twemojis > rendering twemojis")
|
||||
const emojis = new Map()
|
||||
for (const {text:emoji, url} of twemojis.parse(rendered)) {
|
||||
if (!emojis.has(emoji))
|
||||
emojis.set(emoji, (await axios.get(url)).data.replace(/^<svg /, '<svg class="twemoji" '))
|
||||
}
|
||||
//Apply replacements
|
||||
for (const [emoji, twemoji] of emojis) {
|
||||
if (custom)
|
||||
rendered = rendered.replace(new RegExp(`<metrics[ ]*(?<attributes>[^>]*)>${emoji}</metrics>`, "g"), twemoji.replace(/(<svg class="twemoji" [\s\S]+?)(>)/, "$1 $<attributes> $2"))
|
||||
rendered = rendered.replace(new RegExp(emoji, "g"), twemoji)
|
||||
}
|
||||
return rendered
|
||||
},
|
||||
/**Render github emojis */
|
||||
async gemojis(rendered, {rest}) {
|
||||
//Load gemojis
|
||||
console.debug("metrics/svg/gemojis > rendering gemojis")
|
||||
const emojis = new Map()
|
||||
try {
|
||||
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)))
|
||||
emojis.set(emoji, `<img class="gemoji" src="${await imgb64(url)}" height="16" width="16" alt="">`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.debug("metrics/svg/gemojis > could not load gemojis")
|
||||
console.debug(error)
|
||||
}
|
||||
//Apply replacements
|
||||
for (const [emoji, gemoji] of emojis)
|
||||
rendered = rendered.replace(new RegExp(emoji, "g"), gemoji)
|
||||
return rendered
|
||||
},
|
||||
}
|
||||
return {resized:new XMLSerializer().serializeToString(document.querySelector("svg")), height, width}
|
||||
}, padding))
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
console.debug(`metrics/svg/resize > an error occured: ${error}`)
|
||||
throw error
|
||||
}
|
||||
//Convert if required
|
||||
if (convert) {
|
||||
console.debug(`metrics/svg/resize > convert to ${convert}`)
|
||||
resized = await page.screenshot({type:convert, clip:{x:0, y:0, width, height}, omitBackground:true})
|
||||
mime = `image/${convert}`
|
||||
}
|
||||
//Result
|
||||
await page.close()
|
||||
console.debug("metrics/svg/resize > rendering complete")
|
||||
return {resized, mime}
|
||||
},
|
||||
/**Render twemojis */
|
||||
async twemojis(rendered, {custom = true} = {}) {
|
||||
//Load emojis
|
||||
console.debug("metrics/svg/twemojis > rendering twemojis")
|
||||
const emojis = new Map()
|
||||
for (const {text:emoji, url} of twemojis.parse(rendered)) {
|
||||
if (!emojis.has(emoji))
|
||||
emojis.set(emoji, (await axios.get(url)).data.replace(/^<svg /, '<svg class="twemoji" '))
|
||||
}
|
||||
//Apply replacements
|
||||
for (const [emoji, twemoji] of emojis) {
|
||||
if (custom)
|
||||
rendered = rendered.replace(new RegExp(`<metrics[ ]*(?<attributes>[^>]*)>${emoji}</metrics>`, "g"), twemoji.replace(/(<svg class="twemoji" [\s\S]+?)(>)/, "$1 $<attributes> $2"))
|
||||
rendered = rendered.replace(new RegExp(emoji, "g"), twemoji)
|
||||
}
|
||||
return rendered
|
||||
},
|
||||
/**Render github emojis */
|
||||
async gemojis(rendered, {rest}) {
|
||||
//Load gemojis
|
||||
console.debug("metrics/svg/gemojis > rendering gemojis")
|
||||
const emojis = new Map()
|
||||
try {
|
||||
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)))
|
||||
emojis.set(emoji, `<img class="gemoji" src="${await imgb64(url)}" height="16" width="16" alt="">`)
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.debug("metrics/svg/gemojis > could not load gemojis")
|
||||
console.debug(error)
|
||||
}
|
||||
//Apply replacements
|
||||
for (const [emoji, gemoji] of emojis)
|
||||
rendered = rendered.replace(new RegExp(emoji, "g"), gemoji)
|
||||
return rendered
|
||||
},
|
||||
}
|
||||
|
||||
/**Wait */
|
||||
export async function wait(seconds) {
|
||||
await new Promise(solve => setTimeout(solve, seconds*1000))
|
||||
}
|
||||
export async function wait(seconds) {
|
||||
await new Promise(solve => setTimeout(solve, seconds * 1000))
|
||||
}
|
||||
|
||||
/**Create record from puppeteer browser */
|
||||
export async function record({page, width, height, frames, scale = 1, quality = 80, x = 0, y = 0, delay = 150, background = true}) {
|
||||
//Register images frames
|
||||
const images = []
|
||||
for (let i = 0; i < frames; i++) {
|
||||
images.push(await page.screenshot({type:"png", clip:{width, height, x, y}, omitBackground:background}))
|
||||
await wait(delay/1000)
|
||||
if (i%10 === 0)
|
||||
console.debug(`metrics/record > processed ${i}/${frames} frames`)
|
||||
}
|
||||
console.debug(`metrics/record > processed ${frames}/${frames} frames`)
|
||||
//Post-processing
|
||||
console.debug("metrics/record > applying post-processing")
|
||||
return Promise.all(images.map(async buffer => (await jimp.read(buffer)).scale(scale).quality(quality).getBase64Async("image/png")))
|
||||
export async function record({page, width, height, frames, scale = 1, quality = 80, x = 0, y = 0, delay = 150, background = true}) {
|
||||
//Register images frames
|
||||
const images = []
|
||||
for (let i = 0; i < frames; i++) {
|
||||
images.push(await page.screenshot({type:"png", clip:{width, height, x, y}, omitBackground:background}))
|
||||
await wait(delay / 1000)
|
||||
if (i % 10 === 0)
|
||||
console.debug(`metrics/record > processed ${i}/${frames} frames`)
|
||||
}
|
||||
console.debug(`metrics/record > processed ${frames}/${frames} frames`)
|
||||
//Post-processing
|
||||
console.debug("metrics/record > applying post-processing")
|
||||
return Promise.all(images.map(async buffer => (await jimp.read(buffer)).scale(scale).quality(quality).getBase64Async("image/png")))
|
||||
}
|
||||
|
||||
/**Create gif from puppeteer browser*/
|
||||
export async function gif({page, width, height, frames, x = 0, y = 0, repeat = true, delay = 150, quality = 10}) {
|
||||
//Create temporary stream
|
||||
const path = paths.join(os.tmpdir(), `${Math.round(Math.random()*1000000000)}.gif`)
|
||||
console.debug(`metrics/puppeteergif > set write stream to "${path}"`)
|
||||
if (fss.existsSync(path))
|
||||
await fs.unlink(path)
|
||||
//Create encoder
|
||||
const encoder = new GIFEncoder(width, height)
|
||||
encoder.createWriteStream().pipe(fss.createWriteStream(path))
|
||||
encoder.start()
|
||||
encoder.setRepeat(repeat ? 0 : -1)
|
||||
encoder.setDelay(delay)
|
||||
encoder.setQuality(quality)
|
||||
//Register frames
|
||||
for (let i = 0; i < frames; i++) {
|
||||
const buffer = new PNG(await page.screenshot({clip:{width, height, x, y}}))
|
||||
encoder.addFrame(await new Promise(solve => buffer.decode(pixels => solve(pixels))))
|
||||
if (frames%10 === 0)
|
||||
console.debug(`metrics/puppeteergif > processed ${i}/${frames} frames`)
|
||||
}
|
||||
console.debug(`metrics/puppeteergif > processed ${frames}/${frames} frames`)
|
||||
//Close encoder and convert to base64
|
||||
encoder.finish()
|
||||
const result = await fs.readFile(path, "base64")
|
||||
await fs.unlink(path)
|
||||
return `data:image/gif;base64,${result}`
|
||||
export async function gif({page, width, height, frames, x = 0, y = 0, repeat = true, delay = 150, quality = 10}) {
|
||||
//Create temporary stream
|
||||
const path = paths.join(os.tmpdir(), `${Math.round(Math.random() * 1000000000)}.gif`)
|
||||
console.debug(`metrics/puppeteergif > set write stream to "${path}"`)
|
||||
if (fss.existsSync(path))
|
||||
await fs.unlink(path)
|
||||
//Create encoder
|
||||
const encoder = new GIFEncoder(width, height)
|
||||
encoder.createWriteStream().pipe(fss.createWriteStream(path))
|
||||
encoder.start()
|
||||
encoder.setRepeat(repeat ? 0 : -1)
|
||||
encoder.setDelay(delay)
|
||||
encoder.setQuality(quality)
|
||||
//Register frames
|
||||
for (let i = 0; i < frames; i++) {
|
||||
const buffer = new PNG(await page.screenshot({clip:{width, height, x, y}}))
|
||||
encoder.addFrame(await new Promise(solve => buffer.decode(pixels => solve(pixels))))
|
||||
if (frames % 10 === 0)
|
||||
console.debug(`metrics/puppeteergif > processed ${i}/${frames} frames`)
|
||||
}
|
||||
console.debug(`metrics/puppeteergif > processed ${frames}/${frames} frames`)
|
||||
//Close encoder and convert to base64
|
||||
encoder.finish()
|
||||
const result = await fs.readFile(path, "base64")
|
||||
await fs.unlink(path)
|
||||
return `data:image/gif;base64,${result}`
|
||||
}
|
||||
|
||||
@@ -1,66 +1,66 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Last.fm api
|
||||
if (/^https:..ws.audioscrobbler.com.*$/.test(url)) {
|
||||
//Get recently played tracks
|
||||
if (/user.getrecenttracks/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking lastfm api result > ${url}`)
|
||||
const artist = faker.random.word()
|
||||
const album = faker.random.words(3)
|
||||
const track = faker.random.words(5)
|
||||
const date = faker.date.recent()
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
recenttracks:{
|
||||
"@attr":{
|
||||
page:"1",
|
||||
perPage:"1",
|
||||
user:"RJ",
|
||||
total:"100",
|
||||
pages:"100",
|
||||
},
|
||||
track:[
|
||||
{
|
||||
artist:{
|
||||
mbid:"",
|
||||
"#text":artist,
|
||||
},
|
||||
album:{
|
||||
mbid:"",
|
||||
"#text":album,
|
||||
},
|
||||
image:[
|
||||
{
|
||||
size:"small",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"medium",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"large",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"extralarge",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
],
|
||||
streamable:"0",
|
||||
date:{
|
||||
uts:Math.floor(date.getTime() / 1000),
|
||||
"#text":date.toUTCString().slice(5, 22),
|
||||
},
|
||||
url:faker.internet.url(),
|
||||
name:track,
|
||||
mbid:"",
|
||||
},
|
||||
],
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Last.fm api
|
||||
if (/^https:..ws.audioscrobbler.com.*$/.test(url)) {
|
||||
//Get recently played tracks
|
||||
if (/user.getrecenttracks/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking lastfm api result > ${url}`)
|
||||
const artist = faker.random.word()
|
||||
const album = faker.random.words(3)
|
||||
const track = faker.random.words(5)
|
||||
const date = faker.date.recent()
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
recenttracks:{
|
||||
"@attr":{
|
||||
page:"1",
|
||||
perPage:"1",
|
||||
user:"RJ",
|
||||
total:"100",
|
||||
pages:"100",
|
||||
},
|
||||
track:[
|
||||
{
|
||||
artist:{
|
||||
mbid:"",
|
||||
"#text":artist,
|
||||
},
|
||||
album:{
|
||||
mbid:"",
|
||||
"#text":album,
|
||||
},
|
||||
image:[
|
||||
{
|
||||
size:"small",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"medium",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"large",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
{
|
||||
size:"extralarge",
|
||||
"#text":faker.image.abstract(),
|
||||
},
|
||||
],
|
||||
streamable:"0",
|
||||
date:{
|
||||
uts:Math.floor(date.getTime() / 1000),
|
||||
"#text":date.toUTCString().slice(5, 22),
|
||||
},
|
||||
url:faker.internet.url(),
|
||||
name:track,
|
||||
mbid:"",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, url}) {
|
||||
//Last.fm api
|
||||
if (/^https:..testapp.herokuapp.com.*$/.test(url)) {
|
||||
//Get Nightscout Data
|
||||
console.debug(`metrics/compute/mocks > mocking nightscout api result > ${url}`)
|
||||
const lastInterval = Math.floor(new Date() / 300000) * 300000
|
||||
return ({
|
||||
status:200,
|
||||
data:new Array(12).fill(null).map(_ => ({
|
||||
_id:faker.git.commitSha().substring(0, 23),
|
||||
device:"xDrip-DexcomG5",
|
||||
date:lastInterval,
|
||||
dateString:new Date(lastInterval).toISOString(),
|
||||
sgv:faker.datatype.number({min:40, max:400}),
|
||||
delta:faker.datatype.number({min:-10, max:10}),
|
||||
direction:faker.random.arrayElement(["SingleUp", "DoubleUp", "FortyFiveUp", "Flat", "FortyFiveDown", "SingleDown", "DoubleDown"]),
|
||||
type:"sgv",
|
||||
filtered:0,
|
||||
unfiltered:0,
|
||||
rssi:100,
|
||||
noise:1,
|
||||
sysTime:new Date(lastInterval).toISOString(),
|
||||
utcOffset:faker.datatype.number({min:-12, max:14})*60,
|
||||
})),
|
||||
})
|
||||
}
|
||||
//Last.fm api
|
||||
if (/^https:..testapp.herokuapp.com.*$/.test(url)) {
|
||||
//Get Nightscout Data
|
||||
console.debug(`metrics/compute/mocks > mocking nightscout api result > ${url}`)
|
||||
const lastInterval = Math.floor(new Date() / 300000) * 300000
|
||||
return ({
|
||||
status:200,
|
||||
data:new Array(12).fill(null).map(_ => ({
|
||||
_id:faker.git.commitSha().substring(0, 23),
|
||||
device:"xDrip-DexcomG5",
|
||||
date:lastInterval,
|
||||
dateString:new Date(lastInterval).toISOString(),
|
||||
sgv:faker.datatype.number({min:40, max:400}),
|
||||
delta:faker.datatype.number({min:-10, max:10}),
|
||||
direction:faker.random.arrayElement(["SingleUp", "DoubleUp", "FortyFiveUp", "Flat", "FortyFiveDown", "SingleDown", "DoubleDown"]),
|
||||
type:"sgv",
|
||||
filtered:0,
|
||||
unfiltered:0,
|
||||
rssi:100,
|
||||
noise:1,
|
||||
sysTime:new Date(lastInterval).toISOString(),
|
||||
utcOffset:faker.datatype.number({min:-12, max:14}) * 60,
|
||||
})),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,105 +1,105 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Tested url
|
||||
const tested = url.match(/&url=(?<tested>.*?)(?:&|$)/)?.groups?.tested ?? faker.internet.url()
|
||||
//Pagespeed api
|
||||
if (/^https:..www.googleapis.com.pagespeedonline.v5.*$/.test(url)) {
|
||||
//Pagespeed result
|
||||
if (/v5.runPagespeed.*&key=MOCKED_TOKEN/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking pagespeed api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
captchaResult:"CAPTCHA_NOT_NEEDED",
|
||||
id:tested,
|
||||
lighthouseResult:{
|
||||
requestedUrl:tested,
|
||||
finalUrl:tested,
|
||||
lighthouseVersion:"6.3.0",
|
||||
audits:{
|
||||
"final-screenshot":{
|
||||
id:"final-screenshot",
|
||||
title:"Final Screenshot",
|
||||
score:null,
|
||||
details:{
|
||||
data:null,
|
||||
type:"screenshot",
|
||||
timestamp:Date.now(),
|
||||
},
|
||||
},
|
||||
metrics:{
|
||||
id:"metrics",
|
||||
title:"Metrics",
|
||||
score:null,
|
||||
details:{
|
||||
items:[
|
||||
{
|
||||
observedFirstContentfulPaint:faker.datatype.number(500),
|
||||
observedFirstVisualChangeTs:faker.time.recent(),
|
||||
observedFirstContentfulPaintTs:faker.time.recent(),
|
||||
firstContentfulPaint:faker.datatype.number(500),
|
||||
observedDomContentLoaded:faker.datatype.number(500),
|
||||
observedFirstMeaningfulPaint:faker.datatype.number(1000),
|
||||
maxPotentialFID:faker.datatype.number(500),
|
||||
observedLoad:faker.datatype.number(500),
|
||||
firstMeaningfulPaint:faker.datatype.number(500),
|
||||
observedCumulativeLayoutShift:faker.datatype.float({max:1}),
|
||||
observedSpeedIndex:faker.datatype.number(1000),
|
||||
observedSpeedIndexTs:faker.time.recent(),
|
||||
observedTimeOriginTs:faker.time.recent(),
|
||||
observedLargestContentfulPaint:faker.datatype.number(1000),
|
||||
cumulativeLayoutShift:faker.datatype.float({max:1}),
|
||||
observedFirstPaintTs:faker.time.recent(),
|
||||
observedTraceEndTs:faker.time.recent(),
|
||||
largestContentfulPaint:faker.datatype.number(2000),
|
||||
observedTimeOrigin:faker.datatype.number(10),
|
||||
speedIndex:faker.datatype.number(1000),
|
||||
observedTraceEnd:faker.datatype.number(2000),
|
||||
observedDomContentLoadedTs:faker.time.recent(),
|
||||
observedFirstPaint:faker.datatype.number(500),
|
||||
totalBlockingTime:faker.datatype.number(500),
|
||||
observedLastVisualChangeTs:faker.time.recent(),
|
||||
observedFirstVisualChange:faker.datatype.number(500),
|
||||
observedLargestContentfulPaintTs:faker.time.recent(),
|
||||
estimatedInputLatency:faker.datatype.number(100),
|
||||
observedLoadTs:faker.time.recent(),
|
||||
observedLastVisualChange:faker.datatype.number(1000),
|
||||
firstCPUIdle:faker.datatype.number(1000),
|
||||
interactive:faker.datatype.number(1000),
|
||||
observedNavigationStartTs:faker.time.recent(),
|
||||
observedNavigationStart:faker.datatype.number(10),
|
||||
observedFirstMeaningfulPaintTs:faker.time.recent(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
categories:{
|
||||
"best-practices":{
|
||||
id:"best-practices",
|
||||
title:"Best Practices",
|
||||
score:faker.datatype.float({max:1}),
|
||||
},
|
||||
seo:{
|
||||
id:"seo",
|
||||
title:"SEO",
|
||||
score:faker.datatype.float({max:1}),
|
||||
},
|
||||
accessibility:{
|
||||
id:"accessibility",
|
||||
title:"Accessibility",
|
||||
score:faker.datatype.float({max:1}),
|
||||
},
|
||||
performance:{
|
||||
id:"performance",
|
||||
title:"Performance",
|
||||
score:faker.datatype.float({max:1}),
|
||||
},
|
||||
},
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Tested url
|
||||
const tested = url.match(/&url=(?<tested>.*?)(?:&|$)/)?.groups?.tested ?? faker.internet.url()
|
||||
//Pagespeed api
|
||||
if (/^https:..www.googleapis.com.pagespeedonline.v5.*$/.test(url)) {
|
||||
//Pagespeed result
|
||||
if (/v5.runPagespeed.*&key=MOCKED_TOKEN/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking pagespeed api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
captchaResult:"CAPTCHA_NOT_NEEDED",
|
||||
id:tested,
|
||||
lighthouseResult:{
|
||||
requestedUrl:tested,
|
||||
finalUrl:tested,
|
||||
lighthouseVersion:"6.3.0",
|
||||
audits:{
|
||||
"final-screenshot":{
|
||||
id:"final-screenshot",
|
||||
title:"Final Screenshot",
|
||||
score:null,
|
||||
details:{
|
||||
data:null,
|
||||
type:"screenshot",
|
||||
timestamp:Date.now(),
|
||||
},
|
||||
analysisUTCTimestamp:`${faker.date.recent()}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
metrics:{
|
||||
id:"metrics",
|
||||
title:"Metrics",
|
||||
score:null,
|
||||
details:{
|
||||
items:[
|
||||
{
|
||||
observedFirstContentfulPaint:faker.datatype.number(500),
|
||||
observedFirstVisualChangeTs:faker.time.recent(),
|
||||
observedFirstContentfulPaintTs:faker.time.recent(),
|
||||
firstContentfulPaint:faker.datatype.number(500),
|
||||
observedDomContentLoaded:faker.datatype.number(500),
|
||||
observedFirstMeaningfulPaint:faker.datatype.number(1000),
|
||||
maxPotentialFID:faker.datatype.number(500),
|
||||
observedLoad:faker.datatype.number(500),
|
||||
firstMeaningfulPaint:faker.datatype.number(500),
|
||||
observedCumulativeLayoutShift:faker.datatype.float({max:1}),
|
||||
observedSpeedIndex:faker.datatype.number(1000),
|
||||
observedSpeedIndexTs:faker.time.recent(),
|
||||
observedTimeOriginTs:faker.time.recent(),
|
||||
observedLargestContentfulPaint:faker.datatype.number(1000),
|
||||
cumulativeLayoutShift:faker.datatype.float({max:1}),
|
||||
observedFirstPaintTs:faker.time.recent(),
|
||||
observedTraceEndTs:faker.time.recent(),
|
||||
largestContentfulPaint:faker.datatype.number(2000),
|
||||
observedTimeOrigin:faker.datatype.number(10),
|
||||
speedIndex:faker.datatype.number(1000),
|
||||
observedTraceEnd:faker.datatype.number(2000),
|
||||
observedDomContentLoadedTs:faker.time.recent(),
|
||||
observedFirstPaint:faker.datatype.number(500),
|
||||
totalBlockingTime:faker.datatype.number(500),
|
||||
observedLastVisualChangeTs:faker.time.recent(),
|
||||
observedFirstVisualChange:faker.datatype.number(500),
|
||||
observedLargestContentfulPaintTs:faker.time.recent(),
|
||||
estimatedInputLatency:faker.datatype.number(100),
|
||||
observedLoadTs:faker.time.recent(),
|
||||
observedLastVisualChange:faker.datatype.number(1000),
|
||||
firstCPUIdle:faker.datatype.number(1000),
|
||||
interactive:faker.datatype.number(1000),
|
||||
observedNavigationStartTs:faker.time.recent(),
|
||||
observedNavigationStart:faker.datatype.number(10),
|
||||
observedFirstMeaningfulPaintTs:faker.time.recent(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
categories:{
|
||||
"best-practices":{
|
||||
id:"best-practices",
|
||||
title:"Best Practices",
|
||||
score:faker.datatype.float({max:1}),
|
||||
},
|
||||
seo:{
|
||||
id:"seo",
|
||||
title:"SEO",
|
||||
score:faker.datatype.float({max:1}),
|
||||
},
|
||||
accessibility:{
|
||||
id:"accessibility",
|
||||
title:"Accessibility",
|
||||
score:faker.datatype.float({max:1}),
|
||||
},
|
||||
performance:{
|
||||
id:"performance",
|
||||
title:"Performance",
|
||||
score:faker.datatype.float({max:1}),
|
||||
},
|
||||
},
|
||||
},
|
||||
analysisUTCTimestamp:`${faker.date.recent()}`,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,65 +1,65 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Spotify api
|
||||
if (/^https:..api.spotify.com.*$/.test(url)) {
|
||||
//Get recently played tracks
|
||||
if (/me.player.recently-played/.test(url)&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN_ACCESS")) {
|
||||
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
|
||||
const artist = faker.random.words()
|
||||
const track = faker.random.words(5)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
items:[
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Spotify api
|
||||
if (/^https:..api.spotify.com.*$/.test(url)) {
|
||||
//Get recently played tracks
|
||||
if (/me.player.recently-played/.test(url) && (options?.headers?.Authorization === "Bearer MOCKED_TOKEN_ACCESS")) {
|
||||
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
|
||||
const artist = faker.random.words()
|
||||
const track = faker.random.words(5)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
items:[
|
||||
{
|
||||
track:{
|
||||
album:{
|
||||
album_type:"single",
|
||||
artists:[
|
||||
{
|
||||
track:{
|
||||
album:{
|
||||
album_type:"single",
|
||||
artists:[
|
||||
{
|
||||
name:artist,
|
||||
type:"artist",
|
||||
},
|
||||
],
|
||||
images:[
|
||||
{
|
||||
height:640,
|
||||
url:faker.image.abstract(),
|
||||
width:640,
|
||||
},
|
||||
{
|
||||
height:300,
|
||||
url:faker.image.abstract(),
|
||||
width:300,
|
||||
},
|
||||
{
|
||||
height:64,
|
||||
url:faker.image.abstract(),
|
||||
width:64,
|
||||
},
|
||||
],
|
||||
name:track,
|
||||
release_date:`${faker.date.past()}`.substring(0, 10),
|
||||
type:"album",
|
||||
},
|
||||
artists:[
|
||||
{
|
||||
name:artist,
|
||||
type:"artist",
|
||||
},
|
||||
],
|
||||
name:track,
|
||||
preview_url:faker.internet.url(),
|
||||
type:"track",
|
||||
},
|
||||
played_at:`${faker.date.recent()}`,
|
||||
context:{
|
||||
type:"album",
|
||||
},
|
||||
name:artist,
|
||||
type:"artist",
|
||||
},
|
||||
],
|
||||
images:[
|
||||
{
|
||||
height:640,
|
||||
url:faker.image.abstract(),
|
||||
width:640,
|
||||
},
|
||||
{
|
||||
height:300,
|
||||
url:faker.image.abstract(),
|
||||
width:300,
|
||||
},
|
||||
{
|
||||
height:64,
|
||||
url:faker.image.abstract(),
|
||||
width:64,
|
||||
},
|
||||
],
|
||||
name:track,
|
||||
release_date:`${faker.date.past()}`.substring(0, 10),
|
||||
type:"album",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
artists:[
|
||||
{
|
||||
name:artist,
|
||||
type:"artist",
|
||||
},
|
||||
],
|
||||
name:track,
|
||||
preview_url:faker.internet.url(),
|
||||
type:"track",
|
||||
},
|
||||
played_at:`${faker.date.recent()}`,
|
||||
context:{
|
||||
type:"album",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +1,99 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Stackoverflow api
|
||||
if (/^https:..api.stackexchange.com.2.2.*$/.test(url)) {
|
||||
//Extract user id
|
||||
const user_id = url.match(/[/]users[/](?<id>\d+)/)?.groups?.id ?? NaN
|
||||
const pagesize = Number(url.match(/pagesize=(?<pagesize>\d+)/)?.groups?.pagesize) || 30
|
||||
//User account
|
||||
if (/users[/]\d+[/][?]site=stackoverflow$/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
items:[
|
||||
{
|
||||
badge_counts:{bronze:faker.datatype.number(500), silver:faker.datatype.number(300), gold:faker.datatype.number(100)},
|
||||
accept_rate:faker.datatype.number(100),
|
||||
answer_count:faker.datatype.number(1000),
|
||||
question_count:faker.datatype.number(1000),
|
||||
view_count:faker.datatype.number(10000),
|
||||
creation_date:faker.date.past(),
|
||||
display_name:faker.internet.userName(),
|
||||
user_id,
|
||||
reputation:faker.datatype.number(100000),
|
||||
},
|
||||
],
|
||||
has_more:false,
|
||||
quota_max:300,
|
||||
quota_remaining:faker.datatype.number(300),
|
||||
},
|
||||
})
|
||||
}
|
||||
//Total metrics
|
||||
if (/[?]site=stackoverflow&filter=total$/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
total:faker.datatype.number(10000),
|
||||
},
|
||||
})
|
||||
}
|
||||
//Questions
|
||||
if ((/questions[?]site=stackoverflow/.test(url))||(/questions[/][\d;]+[?]site=stackoverflow/.test(url))) {
|
||||
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
items:new Array(pagesize).fill(null).map(_ => ({
|
||||
tags:new Array(5).fill(null).map(_ => faker.lorem.slug()),
|
||||
owner:{display_name:faker.internet.userName()},
|
||||
is_answered:faker.datatype.boolean(),
|
||||
view_count:faker.datatype.number(10000),
|
||||
accepted_answer_id:faker.datatype.number(1000000),
|
||||
answer_count:faker.datatype.number(100),
|
||||
score:faker.datatype.number(1000),
|
||||
creation_date:faker.time.recent(),
|
||||
down_vote_count:faker.datatype.number(1000),
|
||||
up_vote_count:faker.datatype.number(1000),
|
||||
comment_count:faker.datatype.number(1000),
|
||||
favorite_count:faker.datatype.number(1000),
|
||||
title:faker.lorem.sentence(),
|
||||
body_markdown:faker.lorem.paragraphs(),
|
||||
link:faker.internet.url(),
|
||||
question_id:faker.datatype.number(1000000),
|
||||
})),
|
||||
has_more:false,
|
||||
quota_max:300,
|
||||
quota_remaining:faker.datatype.number(300),
|
||||
},
|
||||
})
|
||||
}
|
||||
//Answers
|
||||
if ((/answers[?]site=stackoverflow/.test(url))||(/answers[/][\d;]+[?]site=stackoverflow/.test(url))) {
|
||||
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
items:new Array(pagesize).fill(null).map(_ => ({
|
||||
owner:{display_name:faker.internet.userName()},
|
||||
link:faker.internet.url(),
|
||||
is_accepted:faker.datatype.boolean(),
|
||||
score:faker.datatype.number(1000),
|
||||
down_vote_count:faker.datatype.number(1000),
|
||||
up_vote_count:faker.datatype.number(1000),
|
||||
comment_count:faker.datatype.number(1000),
|
||||
creation_date:faker.time.recent(),
|
||||
question_id:faker.datatype.number(1000000),
|
||||
body_markdown:faker.lorem.paragraphs(),
|
||||
answer_id:faker.datatype.number(1000000),
|
||||
})),
|
||||
has_more:false,
|
||||
quota_max:300,
|
||||
quota_remaining:faker.datatype.number(300),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Stackoverflow api
|
||||
if (/^https:..api.stackexchange.com.2.2.*$/.test(url)) {
|
||||
//Extract user id
|
||||
const user_id = url.match(/[/]users[/](?<id>\d+)/)?.groups?.id ?? NaN
|
||||
const pagesize = Number(url.match(/pagesize=(?<pagesize>\d+)/)?.groups?.pagesize) || 30
|
||||
//User account
|
||||
if (/users[/]\d+[/][?]site=stackoverflow$/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
items:[
|
||||
{
|
||||
badge_counts:{bronze:faker.datatype.number(500), silver:faker.datatype.number(300), gold:faker.datatype.number(100)},
|
||||
accept_rate:faker.datatype.number(100),
|
||||
answer_count:faker.datatype.number(1000),
|
||||
question_count:faker.datatype.number(1000),
|
||||
view_count:faker.datatype.number(10000),
|
||||
creation_date:faker.date.past(),
|
||||
display_name:faker.internet.userName(),
|
||||
user_id,
|
||||
reputation:faker.datatype.number(100000),
|
||||
},
|
||||
],
|
||||
has_more:false,
|
||||
quota_max:300,
|
||||
quota_remaining:faker.datatype.number(300),
|
||||
},
|
||||
})
|
||||
}
|
||||
//Total metrics
|
||||
if (/[?]site=stackoverflow&filter=total$/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
total:faker.datatype.number(10000),
|
||||
},
|
||||
})
|
||||
}
|
||||
//Questions
|
||||
if ((/questions[?]site=stackoverflow/.test(url)) || (/questions[/][\d;]+[?]site=stackoverflow/.test(url))) {
|
||||
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
items:new Array(pagesize).fill(null).map(_ => ({
|
||||
tags:new Array(5).fill(null).map(_ => faker.lorem.slug()),
|
||||
owner:{display_name:faker.internet.userName()},
|
||||
is_answered:faker.datatype.boolean(),
|
||||
view_count:faker.datatype.number(10000),
|
||||
accepted_answer_id:faker.datatype.number(1000000),
|
||||
answer_count:faker.datatype.number(100),
|
||||
score:faker.datatype.number(1000),
|
||||
creation_date:faker.time.recent(),
|
||||
down_vote_count:faker.datatype.number(1000),
|
||||
up_vote_count:faker.datatype.number(1000),
|
||||
comment_count:faker.datatype.number(1000),
|
||||
favorite_count:faker.datatype.number(1000),
|
||||
title:faker.lorem.sentence(),
|
||||
body_markdown:faker.lorem.paragraphs(),
|
||||
link:faker.internet.url(),
|
||||
question_id:faker.datatype.number(1000000),
|
||||
})),
|
||||
has_more:false,
|
||||
quota_max:300,
|
||||
quota_remaining:faker.datatype.number(300),
|
||||
},
|
||||
})
|
||||
}
|
||||
//Answers
|
||||
if ((/answers[?]site=stackoverflow/.test(url)) || (/answers[/][\d;]+[?]site=stackoverflow/.test(url))) {
|
||||
console.debug(`metrics/compute/mocks > mocking stackoverflow api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
items:new Array(pagesize).fill(null).map(_ => ({
|
||||
owner:{display_name:faker.internet.userName()},
|
||||
link:faker.internet.url(),
|
||||
is_accepted:faker.datatype.boolean(),
|
||||
score:faker.datatype.number(1000),
|
||||
down_vote_count:faker.datatype.number(1000),
|
||||
up_vote_count:faker.datatype.number(1000),
|
||||
comment_count:faker.datatype.number(1000),
|
||||
creation_date:faker.time.recent(),
|
||||
question_id:faker.datatype.number(1000000),
|
||||
body_markdown:faker.lorem.paragraphs(),
|
||||
answer_id:faker.datatype.number(1000000),
|
||||
})),
|
||||
has_more:false,
|
||||
quota_max:300,
|
||||
quota_remaining:faker.datatype.number(300),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Twitter api
|
||||
if (/^https:..api.twitter.com.*$/.test(url)) {
|
||||
//Get user profile
|
||||
if ((/users.by.username/.test(url))&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
|
||||
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
|
||||
const username = url.match(/username[/](?<username>.*?)[?]/)?.groups?.username ?? faker.internet.userName()
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
profile_image_url:faker.image.people(),
|
||||
name:faker.name.findName(),
|
||||
verified:faker.datatype.boolean(),
|
||||
id:faker.datatype.number(1000000).toString(),
|
||||
username,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
//Get recent tweets
|
||||
if ((/tweets.search.recent/.test(url))&&(options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
|
||||
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:[
|
||||
{
|
||||
id:faker.datatype.number(100000000000000).toString(),
|
||||
created_at:`${faker.date.recent()}`,
|
||||
entities:{
|
||||
mentions:[
|
||||
{start:22, end:33, username:"lowlighter"},
|
||||
],
|
||||
},
|
||||
text:"Checkout metrics from @lowlighter ! #GitHub",
|
||||
},
|
||||
{
|
||||
id:faker.datatype.number(100000000000000).toString(),
|
||||
created_at:`${faker.date.recent()}`,
|
||||
text:faker.lorem.paragraph(),
|
||||
},
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Twitter api
|
||||
if (/^https:..api.twitter.com.*$/.test(url)) {
|
||||
//Get user profile
|
||||
if ((/users.by.username/.test(url)) && (options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
|
||||
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
|
||||
const username = url.match(/username[/](?<username>.*?)[?]/)?.groups?.username ?? faker.internet.userName()
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
profile_image_url:faker.image.people(),
|
||||
name:faker.name.findName(),
|
||||
verified:faker.datatype.boolean(),
|
||||
id:faker.datatype.number(1000000).toString(),
|
||||
username,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
//Get recent tweets
|
||||
if ((/tweets.search.recent/.test(url)) && (options?.headers?.Authorization === "Bearer MOCKED_TOKEN")) {
|
||||
console.debug(`metrics/compute/mocks > mocking twitter api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:[
|
||||
{
|
||||
id:faker.datatype.number(100000000000000).toString(),
|
||||
created_at:`${faker.date.recent()}`,
|
||||
entities:{
|
||||
mentions:[
|
||||
{start:22, end:33, username:"lowlighter"},
|
||||
],
|
||||
includes:{
|
||||
users:[
|
||||
{
|
||||
id:faker.datatype.number(100000000000000).toString(),
|
||||
name:"lowlighter",
|
||||
username:"lowlighter",
|
||||
},
|
||||
],
|
||||
},
|
||||
meta:{
|
||||
newest_id:faker.datatype.number(100000000000000).toString(),
|
||||
oldest_id:faker.datatype.number(100000000000000).toString(),
|
||||
result_count:2,
|
||||
next_token:"MOCKED_CURSOR",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
text:"Checkout metrics from @lowlighter ! #GitHub",
|
||||
},
|
||||
{
|
||||
id:faker.datatype.number(100000000000000).toString(),
|
||||
created_at:`${faker.date.recent()}`,
|
||||
text:faker.lorem.paragraph(),
|
||||
},
|
||||
],
|
||||
includes:{
|
||||
users:[
|
||||
{
|
||||
id:faker.datatype.number(100000000000000).toString(),
|
||||
name:"lowlighter",
|
||||
username:"lowlighter",
|
||||
},
|
||||
],
|
||||
},
|
||||
meta:{
|
||||
newest_id:faker.datatype.number(100000000000000).toString(),
|
||||
oldest_id:faker.datatype.number(100000000000000).toString(),
|
||||
result_count:2,
|
||||
next_token:"MOCKED_CURSOR",
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,54 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Wakatime api
|
||||
if (/^https:..wakatime.com.api.v1.users..*.stats.*$/.test(url)) {
|
||||
//Get user profile
|
||||
if (/api_key=MOCKED_TOKEN/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking wakatime api result > ${url}`)
|
||||
const stats = array => {
|
||||
const elements = []
|
||||
let results = new Array(4+faker.datatype.number(2)).fill(null).map(_ => ({
|
||||
get digital() {
|
||||
return `${this.hours}:${this.minutes}`
|
||||
},
|
||||
hours:faker.datatype.number(1000), minutes:faker.datatype.number(1000),
|
||||
name:array ? faker.random.arrayElement(array) : faker.random.words(2).replace(/ /g, "-").toLocaleLowerCase(),
|
||||
percent:0, total_seconds:faker.datatype.number(1000000),
|
||||
}))
|
||||
results = results.filter(({name}) => elements.includes(name) ? false : (elements.push(name), true))
|
||||
let percents = 100
|
||||
for (const result of results) {
|
||||
result.percent = 1+faker.datatype.number(percents-1)
|
||||
percents -= result.percent
|
||||
}
|
||||
return results
|
||||
}
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
best_day:{
|
||||
created_at:faker.date.recent(),
|
||||
date:`${faker.date.recent()}`.substring(0, 10),
|
||||
total_seconds:faker.datatype.number(1000000),
|
||||
},
|
||||
categories:stats(),
|
||||
daily_average:faker.datatype.number(12*60*60),
|
||||
daily_average_including_other_language:faker.datatype.number(12*60*60),
|
||||
dependencies:stats(),
|
||||
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"]),
|
||||
machines:stats(),
|
||||
operating_systems:stats(["Mac", "Windows", "Linux"]),
|
||||
project:null,
|
||||
projects:stats(),
|
||||
total_seconds:faker.datatype.number(1000000000),
|
||||
total_seconds_including_other_language:faker.datatype.number(1000000000),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Wakatime api
|
||||
if (/^https:..wakatime.com.api.v1.users..*.stats.*$/.test(url)) {
|
||||
//Get user profile
|
||||
if (/api_key=MOCKED_TOKEN/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking wakatime api result > ${url}`)
|
||||
const stats = array => {
|
||||
const elements = []
|
||||
let results = new Array(4 + faker.datatype.number(2)).fill(null).map(_ => ({
|
||||
get digital() {
|
||||
return `${this.hours}:${this.minutes}`
|
||||
},
|
||||
hours:faker.datatype.number(1000),
|
||||
minutes:faker.datatype.number(1000),
|
||||
name:array ? faker.random.arrayElement(array) : faker.random.words(2).replace(/ /g, "-").toLocaleLowerCase(),
|
||||
percent:0,
|
||||
total_seconds:faker.datatype.number(1000000),
|
||||
}))
|
||||
results = results.filter(({name}) => elements.includes(name) ? false : (elements.push(name), true))
|
||||
let percents = 100
|
||||
for (const result of results) {
|
||||
result.percent = 1 + faker.datatype.number(percents - 1)
|
||||
percents -= result.percent
|
||||
}
|
||||
return results
|
||||
}
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
best_day:{
|
||||
created_at:faker.date.recent(),
|
||||
date:`${faker.date.recent()}`.substring(0, 10),
|
||||
total_seconds:faker.datatype.number(1000000),
|
||||
},
|
||||
categories:stats(),
|
||||
daily_average:faker.datatype.number(12 * 60 * 60),
|
||||
daily_average_including_other_language:faker.datatype.number(12 * 60 * 60),
|
||||
dependencies:stats(),
|
||||
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"]),
|
||||
machines:stats(),
|
||||
operating_systems:stats(["Mac", "Windows", "Linux"]),
|
||||
project:null,
|
||||
projects:stats(),
|
||||
total_seconds:faker.datatype.number(1000000000),
|
||||
total_seconds_including_other_language:faker.datatype.number(1000000000),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,75 +1,75 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Wakatime api
|
||||
if (/^https:..apidojo-yahoo-finance-v1.p.rapidapi.com.stock.v2.*$/.test(url)) {
|
||||
//Get company profile
|
||||
if (/get-profile/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking yahoo finance api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
price:{
|
||||
marketCap:{
|
||||
raw:faker.datatype.number(1000000000),
|
||||
},
|
||||
export default function({faker, url, options, login = faker.internet.userName()}) {
|
||||
//Wakatime api
|
||||
if (/^https:..apidojo-yahoo-finance-v1.p.rapidapi.com.stock.v2.*$/.test(url)) {
|
||||
//Get company profile
|
||||
if (/get-profile/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking yahoo finance api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
price:{
|
||||
marketCap:{
|
||||
raw:faker.datatype.number(1000000000),
|
||||
},
|
||||
symbol:"OCTO",
|
||||
},
|
||||
quoteType:{
|
||||
shortName:faker.company.companyName(),
|
||||
longName:faker.company.companyName(),
|
||||
exchangeTimezoneName:faker.address.timeZone(),
|
||||
symbol:"OCTO",
|
||||
},
|
||||
calendarEvents:{},
|
||||
summaryDetail:{},
|
||||
symbol:"OCTO",
|
||||
assetProfile:{
|
||||
fullTimeEmployees:faker.datatype.number(10000),
|
||||
city:faker.address.city(),
|
||||
country:faker.address.country(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
//Get stock chart
|
||||
if (/get-chart/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking yahoo finance api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
chart:{
|
||||
result:[
|
||||
{
|
||||
meta:{
|
||||
currency:"USD",
|
||||
symbol:"OCTO",
|
||||
regularMarketPrice:faker.datatype.number(10000) / 100,
|
||||
chartPreviousClose:faker.datatype.number(10000) / 100,
|
||||
previousClose:faker.datatype.number(10000) / 100,
|
||||
},
|
||||
quoteType:{
|
||||
shortName:faker.company.companyName(),
|
||||
longName:faker.company.companyName(),
|
||||
exchangeTimezoneName:faker.address.timeZone(),
|
||||
symbol:"OCTO",
|
||||
},
|
||||
calendarEvents:{},
|
||||
summaryDetail:{},
|
||||
symbol:"OCTO",
|
||||
assetProfile:{
|
||||
fullTimeEmployees:faker.datatype.number(10000),
|
||||
city:faker.address.city(),
|
||||
country:faker.address.country(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
//Get stock chart
|
||||
if (/get-chart/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking yahoo finance api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
chart:{
|
||||
result:[
|
||||
timestamp:new Array(1000).fill(Date.now()).map((x, i) => x + i * 60000),
|
||||
indicators:{
|
||||
quote:[
|
||||
{
|
||||
meta:{
|
||||
currency:"USD",
|
||||
symbol:"OCTO",
|
||||
regularMarketPrice:faker.datatype.number(10000)/100,
|
||||
chartPreviousClose:faker.datatype.number(10000)/100,
|
||||
previousClose:faker.datatype.number(10000)/100,
|
||||
close:new Array(1000).fill(null).map(_ => faker.datatype.number(10000) / 100),
|
||||
get low() {
|
||||
return this.close
|
||||
},
|
||||
timestamp:new Array(1000).fill(Date.now()).map((x, i) => x+i*60000),
|
||||
indicators:{
|
||||
quote:[
|
||||
{
|
||||
close:new Array(1000).fill(null).map(_ => faker.datatype.number(10000)/100),
|
||||
get low() {
|
||||
return this.close
|
||||
},
|
||||
get high() {
|
||||
return this.close
|
||||
},
|
||||
get open() {
|
||||
return this.close
|
||||
},
|
||||
volume:[],
|
||||
},
|
||||
],
|
||||
get high() {
|
||||
return this.close
|
||||
},
|
||||
get open() {
|
||||
return this.close
|
||||
},
|
||||
volume:[],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,123 +1,122 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, url, body, login = faker.internet.userName()}) {
|
||||
if (/^https:..graphql.anilist.co.*$/.test(url)) {
|
||||
//Initialization and media generator
|
||||
const {query} = body
|
||||
const media = ({type}) => ({
|
||||
title:{romaji:faker.lorem.words(), english:faker.lorem.words(), native:faker.lorem.words()},
|
||||
description:faker.lorem.paragraphs(),
|
||||
type,
|
||||
status:faker.random.arrayElement(["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]),
|
||||
episodes:100+faker.datatype.number(100),
|
||||
volumes:faker.datatype.number(100),
|
||||
chapters:100+faker.datatype.number(1000),
|
||||
averageScore:faker.datatype.number(100),
|
||||
countryOfOrigin:"JP",
|
||||
genres:new Array(6).fill(null).map(_ => faker.lorem.word()),
|
||||
coverImage:{medium:null},
|
||||
startDate:{year:faker.date.past(20).getFullYear()},
|
||||
})
|
||||
//User statistics query
|
||||
if (/^query Statistics /.test(query)) {
|
||||
console.debug("metrics/compute/mocks > mocking anilist api result > Statistics")
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
User:{
|
||||
id:faker.datatype.number(100000),
|
||||
name:faker.internet.userName(),
|
||||
about:null,
|
||||
statistics:{
|
||||
anime:{
|
||||
count:faker.datatype.number(1000),
|
||||
minutesWatched:faker.datatype.number(100000),
|
||||
episodesWatched:faker.datatype.number(10000),
|
||||
genres:new Array(4).fill(null).map(_ => ({genre:faker.lorem.word()})),
|
||||
},
|
||||
manga:{
|
||||
count:faker.datatype.number(1000),
|
||||
chaptersRead:faker.datatype.number(100000),
|
||||
volumesRead:faker.datatype.number(10000),
|
||||
genres:new Array(4).fill(null).map(_ => ({genre:faker.lorem.word()})),
|
||||
},
|
||||
},
|
||||
export default function({faker, url, body, login = faker.internet.userName()}) {
|
||||
if (/^https:..graphql.anilist.co.*$/.test(url)) {
|
||||
//Initialization and media generator
|
||||
const {query} = body
|
||||
const media = ({type}) => ({
|
||||
title:{romaji:faker.lorem.words(), english:faker.lorem.words(), native:faker.lorem.words()},
|
||||
description:faker.lorem.paragraphs(),
|
||||
type,
|
||||
status:faker.random.arrayElement(["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]),
|
||||
episodes:100 + faker.datatype.number(100),
|
||||
volumes:faker.datatype.number(100),
|
||||
chapters:100 + faker.datatype.number(1000),
|
||||
averageScore:faker.datatype.number(100),
|
||||
countryOfOrigin:"JP",
|
||||
genres:new Array(6).fill(null).map(_ => faker.lorem.word()),
|
||||
coverImage:{medium:null},
|
||||
startDate:{year:faker.date.past(20).getFullYear()},
|
||||
})
|
||||
//User statistics query
|
||||
if (/^query Statistics /.test(query)) {
|
||||
console.debug("metrics/compute/mocks > mocking anilist api result > Statistics")
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
User:{
|
||||
id:faker.datatype.number(100000),
|
||||
name:faker.internet.userName(),
|
||||
about:null,
|
||||
statistics:{
|
||||
anime:{
|
||||
count:faker.datatype.number(1000),
|
||||
minutesWatched:faker.datatype.number(100000),
|
||||
episodesWatched:faker.datatype.number(10000),
|
||||
genres:new Array(4).fill(null).map(_ => ({genre:faker.lorem.word()})),
|
||||
},
|
||||
manga:{
|
||||
count:faker.datatype.number(1000),
|
||||
chaptersRead:faker.datatype.number(100000),
|
||||
volumesRead:faker.datatype.number(10000),
|
||||
genres:new Array(4).fill(null).map(_ => ({genre:faker.lorem.word()})),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
//Favorites characters
|
||||
if (/^query FavoritesCharacters /.test(query)) {
|
||||
console.debug("metrics/compute/mocks > mocking anilist api result > Favorites characters")
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
User:{
|
||||
favourites:{
|
||||
characters:{
|
||||
nodes:new Array(2+faker.datatype.number(16)).fill(null).map(_ => ({
|
||||
name:{full:faker.name.findName(), native:faker.name.findName()},
|
||||
image:{medium:null},
|
||||
})),
|
||||
pageInfo:{currentPage:1, hasNextPage:false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
//Favorites characters
|
||||
if (/^query FavoritesCharacters /.test(query)) {
|
||||
console.debug("metrics/compute/mocks > mocking anilist api result > Favorites characters")
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
User:{
|
||||
favourites:{
|
||||
characters:{
|
||||
nodes:new Array(2 + faker.datatype.number(16)).fill(null).map(_ => ({
|
||||
name:{full:faker.name.findName(), native:faker.name.findName()},
|
||||
image:{medium:null},
|
||||
})),
|
||||
pageInfo:{currentPage:1, hasNextPage:false},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
//Favorites anime/manga query
|
||||
if (/^query Favorites /.test(query)) {
|
||||
console.debug("metrics/compute/mocks > mocking anilist api result > Favorites")
|
||||
const type = /anime[(]/.test(query) ? "ANIME" : /manga[(]/.test(query) ? "MANGA" : "OTHER"
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
User:{
|
||||
favourites:{
|
||||
[type.toLocaleLowerCase()]:{
|
||||
nodes:new Array(16).fill(null).map(_ => media({type})),
|
||||
pageInfo:{currentPage:1, hasNextPage:false},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
//Favorites anime/manga query
|
||||
if (/^query Favorites /.test(query)) {
|
||||
console.debug("metrics/compute/mocks > mocking anilist api result > Favorites")
|
||||
const type = /anime[(]/.test(query) ? "ANIME" : /manga[(]/.test(query) ? "MANGA" : "OTHER"
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
User:{
|
||||
favourites:{
|
||||
[type.toLocaleLowerCase()]:{
|
||||
nodes:new Array(16).fill(null).map(_ => media({type})),
|
||||
pageInfo:{currentPage:1, hasNextPage:false},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
//Medias query
|
||||
if (/^query Medias /.test(query)) {
|
||||
console.debug("metrics/compute/mocks > mocking anilist api result > Medias")
|
||||
const {type} = body.variables
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
MediaListCollection:{
|
||||
lists:[
|
||||
{
|
||||
name:{ANIME:"Watching", MANGA:"Reading", OTHER:"Completed"}[type],
|
||||
isCustomList:false,
|
||||
entries:new Array(16).fill(null).map(_ => ({
|
||||
status:faker.random.arrayElement(["CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"]),
|
||||
progress:faker.datatype.number(100),
|
||||
progressVolumes:null,
|
||||
score:0,
|
||||
startedAt:{year:null, month:null, day:null},
|
||||
completedAt:{year:null, month:null, day:null},
|
||||
media:media({type}),
|
||||
})),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
//Medias query
|
||||
if (/^query Medias /.test(query)) {
|
||||
console.debug("metrics/compute/mocks > mocking anilist api result > Medias")
|
||||
const {type} = body.variables
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
MediaListCollection:{
|
||||
lists:[
|
||||
{
|
||||
name:{ANIME:"Watching", MANGA:"Reading", OTHER:"Completed"}[type],
|
||||
isCustomList:false,
|
||||
entries:new Array(16).fill(null).map(_ => ({
|
||||
status:faker.random.arrayElement(["CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"]),
|
||||
progress:faker.datatype.number(100),
|
||||
progressVolumes:null,
|
||||
score:0,
|
||||
startedAt:{year:null, month:null, day:null},
|
||||
completedAt:{year:null, month:null, day:null},
|
||||
media:media({type}),
|
||||
})),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, url, body, login = faker.internet.userName()}) {
|
||||
if (/^https:..api.hashnode.com.*$/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking hashnode result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
user:{
|
||||
publication:{
|
||||
posts:new Array(30).fill(null).map(_ => ({
|
||||
title:faker.lorem.sentence(),
|
||||
brief:faker.lorem.paragraph(),
|
||||
coverImage:null,
|
||||
dateAdded:faker.date.recent(),
|
||||
})),
|
||||
},
|
||||
},
|
||||
export default function({faker, url, body, login = faker.internet.userName()}) {
|
||||
if (/^https:..api.hashnode.com.*$/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking hashnode result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
data:{
|
||||
user:{
|
||||
publication:{
|
||||
posts:new Array(30).fill(null).map(_ => ({
|
||||
title:faker.lorem.sentence(),
|
||||
brief:faker.lorem.paragraph(),
|
||||
coverImage:null,
|
||||
dateAdded:faker.date.recent(),
|
||||
})),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
//Imports
|
||||
import urls from "url"
|
||||
import urls from "url"
|
||||
|
||||
/**Mocked data */
|
||||
export default function({faker, url, body, login = faker.internet.userName()}) {
|
||||
if (/^https:..accounts.spotify.com.api.token.*$/.test(url)) {
|
||||
//Access token generator
|
||||
const params = new urls.URLSearchParams(body)
|
||||
if ((params.get("grant_type") === "refresh_token")&&(params.get("client_id") === "MOCKED_CLIENT_ID")&&(params.get("client_secret") === "MOCKED_CLIENT_SECRET")&&(params.get("refresh_token") === "MOCKED_REFRESH_TOKEN")) {
|
||||
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
access_token:"MOCKED_TOKEN_ACCESS",
|
||||
token_type:"Bearer",
|
||||
expires_in:3600,
|
||||
scope:"user-read-recently-played user-read-private",
|
||||
},
|
||||
})
|
||||
}
|
||||
export default function({faker, url, body, login = faker.internet.userName()}) {
|
||||
if (/^https:..accounts.spotify.com.api.token.*$/.test(url)) {
|
||||
//Access token generator
|
||||
const params = new urls.URLSearchParams(body)
|
||||
if ((params.get("grant_type") === "refresh_token") && (params.get("client_id") === "MOCKED_CLIENT_ID") && (params.get("client_secret") === "MOCKED_CLIENT_SECRET") && (params.get("refresh_token") === "MOCKED_REFRESH_TOKEN")) {
|
||||
console.debug(`metrics/compute/mocks > mocking spotify api result > ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
data:{
|
||||
access_token:"MOCKED_TOKEN_ACCESS",
|
||||
token_type:"Bearer",
|
||||
expires_in:3600,
|
||||
scope:"user-read-recently-played user-read-private",
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics")
|
||||
return ({
|
||||
user:{
|
||||
repositories:{
|
||||
nodes:[
|
||||
{
|
||||
createdAt:faker.date.recent(),
|
||||
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
|
||||
},
|
||||
],
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
forks:{
|
||||
nodes:[
|
||||
{
|
||||
createdAt:faker.date.recent(),
|
||||
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
|
||||
},
|
||||
],
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
popular:{
|
||||
nodes:[{stargazers:{totalCount:faker.datatype.number(50000)}}],
|
||||
},
|
||||
pullRequests:{
|
||||
nodes:[
|
||||
{
|
||||
createdAt:faker.date.recent(),
|
||||
title:faker.lorem.sentence(),
|
||||
repository:{nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`},
|
||||
},
|
||||
],
|
||||
totalCount:faker.datatype.number(50000),
|
||||
},
|
||||
contributionsCollection:{
|
||||
pullRequestReviewContributions:{
|
||||
nodes:[
|
||||
{
|
||||
occurredAt:faker.date.recent(),
|
||||
pullRequest:{
|
||||
title:faker.lorem.sentence(),
|
||||
number:faker.datatype.number(1000),
|
||||
repository:{nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`},
|
||||
},
|
||||
},
|
||||
],
|
||||
totalCount:faker.datatype.number(1000),
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics")
|
||||
return ({
|
||||
user:{
|
||||
repositories:{
|
||||
nodes:[
|
||||
{
|
||||
createdAt:faker.date.recent(),
|
||||
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
|
||||
},
|
||||
},
|
||||
projects:{totalCount:faker.datatype.number(100)},
|
||||
packages:{totalCount:faker.datatype.number(100)},
|
||||
organizations:{nodes:[], totalCount:faker.datatype.number(5)},
|
||||
gists:{
|
||||
nodes:[{createdAt:faker.date.recent(), name:faker.lorem.slug()}],
|
||||
],
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
forks:{
|
||||
nodes:[
|
||||
{
|
||||
createdAt:faker.date.recent(),
|
||||
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
|
||||
},
|
||||
],
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
popular:{
|
||||
nodes:[{stargazers:{totalCount:faker.datatype.number(50000)}}],
|
||||
},
|
||||
pullRequests:{
|
||||
nodes:[
|
||||
{
|
||||
createdAt:faker.date.recent(),
|
||||
title:faker.lorem.sentence(),
|
||||
repository:{nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`},
|
||||
},
|
||||
],
|
||||
totalCount:faker.datatype.number(50000),
|
||||
},
|
||||
contributionsCollection:{
|
||||
pullRequestReviewContributions:{
|
||||
nodes:[
|
||||
{
|
||||
occurredAt:faker.date.recent(),
|
||||
pullRequest:{
|
||||
title:faker.lorem.sentence(),
|
||||
number:faker.datatype.number(1000),
|
||||
repository:{nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`},
|
||||
},
|
||||
},
|
||||
],
|
||||
totalCount:faker.datatype.number(1000),
|
||||
},
|
||||
starredRepositories:{totalCount:faker.datatype.number(1000)},
|
||||
followers:{totalCount:faker.datatype.number(10000)},
|
||||
following:{totalCount:faker.datatype.number(10000)},
|
||||
bio:faker.lorem.sentence(),
|
||||
status:{message:faker.lorem.paragraph()},
|
||||
sponsorshipsAsSponsor:{totalCount:faker.datatype.number(100)},
|
||||
},
|
||||
})
|
||||
}
|
||||
projects:{totalCount:faker.datatype.number(100)},
|
||||
packages:{totalCount:faker.datatype.number(100)},
|
||||
organizations:{nodes:[], totalCount:faker.datatype.number(5)},
|
||||
gists:{
|
||||
nodes:[{createdAt:faker.date.recent(), name:faker.lorem.slug()}],
|
||||
totalCount:faker.datatype.number(1000),
|
||||
},
|
||||
starredRepositories:{totalCount:faker.datatype.number(1000)},
|
||||
followers:{totalCount:faker.datatype.number(10000)},
|
||||
following:{totalCount:faker.datatype.number(10000)},
|
||||
bio:faker.lorem.sentence(),
|
||||
status:{message:faker.lorem.paragraph()},
|
||||
sponsorshipsAsSponsor:{totalCount:faker.datatype.number(100)},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics")
|
||||
return ({
|
||||
repository:{viewerHasStarred:faker.datatype.boolean()},
|
||||
viewer:{login},
|
||||
})
|
||||
}
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/metrics")
|
||||
return ({
|
||||
repository:{viewerHasStarred:faker.datatype.boolean()},
|
||||
viewer:{login},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/octocat")
|
||||
return ({
|
||||
user:{viewerIsFollowing:faker.datatype.boolean()},
|
||||
viewer:{login},
|
||||
})
|
||||
}
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/octocat")
|
||||
return ({
|
||||
user:{viewerIsFollowing:faker.datatype.boolean()},
|
||||
viewer:{login},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/organizations")
|
||||
return ({
|
||||
organization:{
|
||||
repositories:{
|
||||
nodes:[
|
||||
{
|
||||
createdAt:faker.date.recent(),
|
||||
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
|
||||
},
|
||||
],
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
forks:{
|
||||
nodes:[
|
||||
{
|
||||
createdAt:faker.date.recent(),
|
||||
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
|
||||
},
|
||||
],
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
popular:{
|
||||
nodes:[{stargazers:{totalCount:faker.datatype.number(50000)}}],
|
||||
},
|
||||
projects:{totalCount:faker.datatype.number(100)},
|
||||
packages:{totalCount:faker.datatype.number(100)},
|
||||
membersWithRole:{totalCount:faker.datatype.number(100)},
|
||||
sponsorshipsAsSponsor:{totalCount:faker.datatype.number(100)},
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/organizations")
|
||||
return ({
|
||||
organization:{
|
||||
repositories:{
|
||||
nodes:[
|
||||
{
|
||||
createdAt:faker.date.recent(),
|
||||
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
|
||||
},
|
||||
],
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
})
|
||||
}
|
||||
forks:{
|
||||
nodes:[
|
||||
{
|
||||
createdAt:faker.date.recent(),
|
||||
nameWithOwner:`${faker.internet.userName()}/${faker.lorem.slug()}`,
|
||||
},
|
||||
],
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
popular:{
|
||||
nodes:[{stargazers:{totalCount:faker.datatype.number(50000)}}],
|
||||
},
|
||||
projects:{totalCount:faker.datatype.number(100)},
|
||||
packages:{totalCount:faker.datatype.number(100)},
|
||||
membersWithRole:{totalCount:faker.datatype.number(100)},
|
||||
sponsorshipsAsSponsor:{totalCount:faker.datatype.number(100)},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/ranking")
|
||||
return ({
|
||||
repo_rank:{repositoryCount:faker.datatype.number(100000)},
|
||||
forks_rank:{repositoryCount:faker.datatype.number(100000)},
|
||||
created_rank:{userCount:faker.datatype.number(100000)},
|
||||
user_rank:{userCount:faker.datatype.number(100000)},
|
||||
repo_total:{repositoryCount:faker.datatype.number(100000)},
|
||||
user_total:{userCount:faker.datatype.number(100000)},
|
||||
})
|
||||
}
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > achievements/ranking")
|
||||
return ({
|
||||
repo_rank:{repositoryCount:faker.datatype.number(100000)},
|
||||
forks_rank:{repositoryCount:faker.datatype.number(100000)},
|
||||
created_rank:{userCount:faker.datatype.number(100000)},
|
||||
user_rank:{userCount:faker.datatype.number(100000)},
|
||||
repo_total:{repositoryCount:faker.datatype.number(100000)},
|
||||
user_total:{userCount:faker.datatype.number(100000)},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > base/repositories")
|
||||
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > base/repositories")
|
||||
return /after: "MOCKED_CURSOR"/m.test(query)
|
||||
? ({
|
||||
user:{
|
||||
repositories:{
|
||||
edges:[],
|
||||
nodes:[],
|
||||
},
|
||||
},
|
||||
}) : ({
|
||||
})
|
||||
: ({
|
||||
user:{
|
||||
repositories:{
|
||||
edges:[
|
||||
@@ -46,4 +48,4 @@
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > base/repository")
|
||||
return ({
|
||||
user:{
|
||||
repository:{
|
||||
name:"metrics",
|
||||
owner:{login},
|
||||
createdAt:new Date().toISOString(),
|
||||
diskUsage:Math.floor(Math.random()*10000),
|
||||
homepageUrl:faker.internet.url(),
|
||||
watchers:{totalCount:faker.datatype.number(1000)},
|
||||
stargazers:{totalCount:faker.datatype.number(10000)},
|
||||
languages:{
|
||||
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()}},
|
||||
],
|
||||
},
|
||||
issues_open:{totalCount:faker.datatype.number(100)},
|
||||
issues_closed:{totalCount:faker.datatype.number(100)},
|
||||
pr_open:{totalCount:faker.datatype.number(100)},
|
||||
pr_closed:{totalCount:faker.datatype.number(100)},
|
||||
pr_merged:{totalCount:faker.datatype.number(100)},
|
||||
releases:{totalCount:faker.datatype.number(100)},
|
||||
forkCount:faker.datatype.number(100),
|
||||
licenseInfo:{spdxId:"MIT"},
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > base/repository")
|
||||
return ({
|
||||
user:{
|
||||
repository:{
|
||||
name:"metrics",
|
||||
owner:{login},
|
||||
createdAt:new Date().toISOString(),
|
||||
diskUsage:Math.floor(Math.random() * 10000),
|
||||
homepageUrl:faker.internet.url(),
|
||||
watchers:{totalCount:faker.datatype.number(1000)},
|
||||
stargazers:{totalCount:faker.datatype.number(10000)},
|
||||
languages:{
|
||||
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()}},
|
||||
],
|
||||
},
|
||||
issues_open:{totalCount:faker.datatype.number(100)},
|
||||
issues_closed:{totalCount:faker.datatype.number(100)},
|
||||
pr_open:{totalCount:faker.datatype.number(100)},
|
||||
pr_closed:{totalCount:faker.datatype.number(100)},
|
||||
pr_merged:{totalCount:faker.datatype.number(100)},
|
||||
releases:{totalCount:faker.datatype.number(100)},
|
||||
forkCount:faker.datatype.number(100),
|
||||
licenseInfo:{spdxId:"MIT"},
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,68 +1,68 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > base/user")
|
||||
return ({
|
||||
user:{
|
||||
databaseId:faker.datatype.number(10000000),
|
||||
name:faker.name.findName(),
|
||||
login,
|
||||
createdAt:`${faker.date.past(10)}`,
|
||||
avatarUrl:faker.image.people(),
|
||||
websiteUrl:faker.internet.url(),
|
||||
isHireable:faker.datatype.boolean(),
|
||||
twitterUsername:login,
|
||||
repositories:{totalCount:faker.datatype.number(100), totalDiskUsage:faker.datatype.number(100000), nodes:[]},
|
||||
packages:{totalCount:faker.datatype.number(10)},
|
||||
starredRepositories:{totalCount:faker.datatype.number(1000)},
|
||||
watching:{totalCount:faker.datatype.number(100)},
|
||||
sponsorshipsAsSponsor:{totalCount:faker.datatype.number(10)},
|
||||
sponsorshipsAsMaintainer:{totalCount:faker.datatype.number(10)},
|
||||
contributionsCollection:{
|
||||
totalRepositoriesWithContributedCommits:faker.datatype.number(100),
|
||||
totalCommitContributions:faker.datatype.number(10000),
|
||||
restrictedContributionsCount:faker.datatype.number(10000),
|
||||
totalIssueContributions:faker.datatype.number(100),
|
||||
totalPullRequestContributions:faker.datatype.number(1000),
|
||||
totalPullRequestReviewContributions:faker.datatype.number(1000),
|
||||
},
|
||||
calendar:{
|
||||
contributionCalendar:{
|
||||
weeks:[
|
||||
{
|
||||
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"])},
|
||||
],
|
||||
},
|
||||
{
|
||||
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"])},
|
||||
],
|
||||
},
|
||||
{
|
||||
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"])},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
repositoriesContributedTo:{totalCount:faker.datatype.number(100)},
|
||||
followers:{totalCount:faker.datatype.number(1000)},
|
||||
following:{totalCount:faker.datatype.number(1000)},
|
||||
issueComments:{totalCount:faker.datatype.number(1000)},
|
||||
organizations:{totalCount:faker.datatype.number(10)},
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > base/user")
|
||||
return ({
|
||||
user:{
|
||||
databaseId:faker.datatype.number(10000000),
|
||||
name:faker.name.findName(),
|
||||
login,
|
||||
createdAt:`${faker.date.past(10)}`,
|
||||
avatarUrl:faker.image.people(),
|
||||
websiteUrl:faker.internet.url(),
|
||||
isHireable:faker.datatype.boolean(),
|
||||
twitterUsername:login,
|
||||
repositories:{totalCount:faker.datatype.number(100), totalDiskUsage:faker.datatype.number(100000), nodes:[]},
|
||||
packages:{totalCount:faker.datatype.number(10)},
|
||||
starredRepositories:{totalCount:faker.datatype.number(1000)},
|
||||
watching:{totalCount:faker.datatype.number(100)},
|
||||
sponsorshipsAsSponsor:{totalCount:faker.datatype.number(10)},
|
||||
sponsorshipsAsMaintainer:{totalCount:faker.datatype.number(10)},
|
||||
contributionsCollection:{
|
||||
totalRepositoriesWithContributedCommits:faker.datatype.number(100),
|
||||
totalCommitContributions:faker.datatype.number(10000),
|
||||
restrictedContributionsCount:faker.datatype.number(10000),
|
||||
totalIssueContributions:faker.datatype.number(100),
|
||||
totalPullRequestContributions:faker.datatype.number(1000),
|
||||
totalPullRequestReviewContributions:faker.datatype.number(1000),
|
||||
},
|
||||
})
|
||||
}
|
||||
calendar:{
|
||||
contributionCalendar:{
|
||||
weeks:[
|
||||
{
|
||||
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"])},
|
||||
],
|
||||
},
|
||||
{
|
||||
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"])},
|
||||
],
|
||||
},
|
||||
{
|
||||
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"])},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
repositoriesContributedTo:{totalCount:faker.datatype.number(100)},
|
||||
followers:{totalCount:faker.datatype.number(1000)},
|
||||
following:{totalCount:faker.datatype.number(1000)},
|
||||
issueComments:{totalCount:faker.datatype.number(1000)},
|
||||
organizations:{totalCount:faker.datatype.number(10)},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > contributors/commit")
|
||||
return ({
|
||||
repository:{
|
||||
object:{
|
||||
oid:"MOCKED_SHA",
|
||||
abbreviatedOid:"MOCKED_SHA",
|
||||
messageHeadline:faker.lorem.sentence(),
|
||||
committedDate:faker.date.recent(),
|
||||
},
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > contributors/commit")
|
||||
return ({
|
||||
repository:{
|
||||
object:{
|
||||
oid:"MOCKED_SHA",
|
||||
abbreviatedOid:"MOCKED_SHA",
|
||||
messageHeadline:faker.lorem.sentence(),
|
||||
committedDate:faker.date.recent(),
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > followup/user")
|
||||
return ({
|
||||
user:{
|
||||
issues_open:{totalCount:faker.datatype.number(100)},
|
||||
issues_closed:{totalCount:faker.datatype.number(100)},
|
||||
pr_open:{totalCount:faker.datatype.number(100)},
|
||||
pr_closed:{totalCount:faker.datatype.number(100)},
|
||||
pr_merged:{totalCount:faker.datatype.number(100)},
|
||||
},
|
||||
})
|
||||
}
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > followup/user")
|
||||
return ({
|
||||
user:{
|
||||
issues_open:{totalCount:faker.datatype.number(100)},
|
||||
issues_closed:{totalCount:faker.datatype.number(100)},
|
||||
pr_open:{totalCount:faker.datatype.number(100)},
|
||||
pr_closed:{totalCount:faker.datatype.number(100)},
|
||||
pr_merged:{totalCount:faker.datatype.number(100)},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > gists/default")
|
||||
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > gists/default")
|
||||
return /after: "MOCKED_CURSOR"/m.test(query)
|
||||
? ({
|
||||
user:{
|
||||
gists:{
|
||||
edges:[],
|
||||
nodes:[],
|
||||
},
|
||||
},
|
||||
}) : ({
|
||||
})
|
||||
: ({
|
||||
user:{
|
||||
gists:{
|
||||
edges:[
|
||||
@@ -36,4 +38,4 @@
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/organization")
|
||||
return ({
|
||||
organization:{
|
||||
description:faker.lorem.sentences(),
|
||||
},
|
||||
})
|
||||
}
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/organization")
|
||||
return ({
|
||||
organization:{
|
||||
description:faker.lorem.sentences(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/repository")
|
||||
return ({
|
||||
repository:{
|
||||
description:faker.lorem.sentences(),
|
||||
},
|
||||
})
|
||||
}
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/repository")
|
||||
return ({
|
||||
repository:{
|
||||
description:faker.lorem.sentences(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/user")
|
||||
return ({
|
||||
user:{
|
||||
bio:faker.lorem.sentences(),
|
||||
},
|
||||
})
|
||||
}
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > introduction/user")
|
||||
return ({
|
||||
user:{
|
||||
bio:faker.lorem.sentences(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > isocalendar/calendar")
|
||||
//Generate calendar
|
||||
const date = new Date(query.match(/from: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
|
||||
const to = new Date(query.match(/to: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
|
||||
const weeks = []
|
||||
let contributionDays = []
|
||||
for (; date <= to; date.setDate(date.getDate()+1)) {
|
||||
//Create new week on sunday
|
||||
if (date.getDay() === 0) {
|
||||
weeks.push({contributionDays})
|
||||
contributionDays = []
|
||||
}
|
||||
//Random contributions
|
||||
const contributionCount = Math.min(10, Math.max(0, faker.datatype.number(14)-4))
|
||||
contributionDays.push({
|
||||
contributionCount,
|
||||
color:["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"][Math.ceil(contributionCount/10/0.25)],
|
||||
date:date.toISOString().substring(0, 10),
|
||||
})
|
||||
}
|
||||
return ({
|
||||
user:{
|
||||
calendar:{
|
||||
contributionCalendar:{
|
||||
weeks,
|
||||
},
|
||||
},
|
||||
},
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > isocalendar/calendar")
|
||||
//Generate calendar
|
||||
const date = new Date(query.match(/from: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
|
||||
const to = new Date(query.match(/to: "(?<date>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z)"/)?.groups?.date)
|
||||
const weeks = []
|
||||
let contributionDays = []
|
||||
for (; date <= to; date.setDate(date.getDate() + 1)) {
|
||||
//Create new week on sunday
|
||||
if (date.getDay() === 0) {
|
||||
weeks.push({contributionDays})
|
||||
contributionDays = []
|
||||
}
|
||||
//Random contributions
|
||||
const contributionCount = Math.min(10, Math.max(0, faker.datatype.number(14) - 4))
|
||||
contributionDays.push({
|
||||
contributionCount,
|
||||
color:["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"][Math.ceil(contributionCount / 10 / 0.25)],
|
||||
date:date.toISOString().substring(0, 10),
|
||||
})
|
||||
}
|
||||
return ({
|
||||
user:{
|
||||
calendar:{
|
||||
contributionCalendar:{
|
||||
weeks,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,278 +1,278 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > licenses/default")
|
||||
return ({
|
||||
licenses:[
|
||||
{
|
||||
spdxId:"AGPL-3.0",
|
||||
name:"GNU Affero General Public License v3.0",
|
||||
nickname:"GNU AGPLv3",
|
||||
key:"agpl-3.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"document-changes", label:"State changes"},
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"network-use-disclose", label:"Network use is distribution"},
|
||||
{key:"same-license", label:"Same license"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"Apache-2.0",
|
||||
name:"Apache License 2.0",
|
||||
nickname:null,
|
||||
key:"apache-2.0",
|
||||
limitations:[
|
||||
{key:"trademark-use", label:"Trademark use"},
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"document-changes", label:"State changes"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"BSD-2-Clause",
|
||||
name:'BSD 2-Clause "Simplified" License',
|
||||
nickname:null,
|
||||
key:"bsd-2-clause",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"BSD-3-Clause",
|
||||
name:'BSD 3-Clause "New" or "Revised" License',
|
||||
nickname:null,
|
||||
key:"bsd-3-clause",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"BSL-1.0",
|
||||
name:"Boost Software License 1.0",
|
||||
nickname:null,
|
||||
key:"bsl-1.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright--source", label:"License and copyright notice for source"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"CC0-1.0",
|
||||
name:"Creative Commons Zero v1.0 Universal",
|
||||
nickname:null,
|
||||
key:"cc0-1.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"trademark-use", label:"Trademark use"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"EPL-2.0",
|
||||
name:"Eclipse Public License 2.0",
|
||||
nickname:null,
|
||||
key:"epl-2.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"same-license", label:"Same license"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"GPL-2.0",
|
||||
name:"GNU General Public License v2.0",
|
||||
nickname:"GNU GPLv2",
|
||||
key:"gpl-2.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"document-changes", label:"State changes"},
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"same-license", label:"Same license"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"GPL-3.0",
|
||||
name:"GNU General Public License v3.0",
|
||||
nickname:"GNU GPLv3",
|
||||
key:"gpl-3.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"document-changes", label:"State changes"},
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"same-license", label:"Same license"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"LGPL-2.1",
|
||||
name:"GNU Lesser General Public License v2.1",
|
||||
nickname:"GNU LGPLv2.1",
|
||||
key:"lgpl-2.1",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"document-changes", label:"State changes"},
|
||||
{key:"same-license--library", label:"Same license (library)"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"MIT",
|
||||
name:"MIT License",
|
||||
nickname:null,
|
||||
key:"mit",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"MPL-2.0",
|
||||
name:"Mozilla Public License 2.0",
|
||||
nickname:null,
|
||||
key:"mpl-2.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"trademark-use", label:"Trademark use"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"same-license--file", label:"Same license (file)"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"Unlicense",
|
||||
name:"The Unlicense",
|
||||
nickname:null,
|
||||
key:"unlicense",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[],
|
||||
permissions:[
|
||||
{key:"private-use", label:"Private use"},
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > licenses/default")
|
||||
return ({
|
||||
licenses:[
|
||||
{
|
||||
spdxId:"AGPL-3.0",
|
||||
name:"GNU Affero General Public License v3.0",
|
||||
nickname:"GNU AGPLv3",
|
||||
key:"agpl-3.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"document-changes", label:"State changes"},
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"network-use-disclose", label:"Network use is distribution"},
|
||||
{key:"same-license", label:"Same license"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"Apache-2.0",
|
||||
name:"Apache License 2.0",
|
||||
nickname:null,
|
||||
key:"apache-2.0",
|
||||
limitations:[
|
||||
{key:"trademark-use", label:"Trademark use"},
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"document-changes", label:"State changes"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"BSD-2-Clause",
|
||||
name:'BSD 2-Clause "Simplified" License',
|
||||
nickname:null,
|
||||
key:"bsd-2-clause",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"BSD-3-Clause",
|
||||
name:'BSD 3-Clause "New" or "Revised" License',
|
||||
nickname:null,
|
||||
key:"bsd-3-clause",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"BSL-1.0",
|
||||
name:"Boost Software License 1.0",
|
||||
nickname:null,
|
||||
key:"bsl-1.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright--source", label:"License and copyright notice for source"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"CC0-1.0",
|
||||
name:"Creative Commons Zero v1.0 Universal",
|
||||
nickname:null,
|
||||
key:"cc0-1.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"trademark-use", label:"Trademark use"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"EPL-2.0",
|
||||
name:"Eclipse Public License 2.0",
|
||||
nickname:null,
|
||||
key:"epl-2.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"same-license", label:"Same license"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"GPL-2.0",
|
||||
name:"GNU General Public License v2.0",
|
||||
nickname:"GNU GPLv2",
|
||||
key:"gpl-2.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"document-changes", label:"State changes"},
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"same-license", label:"Same license"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"GPL-3.0",
|
||||
name:"GNU General Public License v3.0",
|
||||
nickname:"GNU GPLv3",
|
||||
key:"gpl-3.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"document-changes", label:"State changes"},
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"same-license", label:"Same license"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"LGPL-2.1",
|
||||
name:"GNU Lesser General Public License v2.1",
|
||||
nickname:"GNU LGPLv2.1",
|
||||
key:"lgpl-2.1",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"document-changes", label:"State changes"},
|
||||
{key:"same-license--library", label:"Same license (library)"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"MIT",
|
||||
name:"MIT License",
|
||||
nickname:null,
|
||||
key:"mit",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"MPL-2.0",
|
||||
name:"Mozilla Public License 2.0",
|
||||
nickname:null,
|
||||
key:"mpl-2.0",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"trademark-use", label:"Trademark use"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[
|
||||
{key:"disclose-source", label:"Disclose source"},
|
||||
{key:"include-copyright", label:"License and copyright notice"},
|
||||
{key:"same-license--file", label:"Same license (file)"},
|
||||
],
|
||||
permissions:[
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
{key:"patent-use", label:"Patent use"},
|
||||
{key:"private-use", label:"Private use"},
|
||||
],
|
||||
},
|
||||
{
|
||||
spdxId:"Unlicense",
|
||||
name:"The Unlicense",
|
||||
nickname:null,
|
||||
key:"unlicense",
|
||||
limitations:[
|
||||
{key:"liability", label:"Liability"},
|
||||
{key:"warranty", label:"Warranty"},
|
||||
],
|
||||
conditions:[],
|
||||
permissions:[
|
||||
{key:"private-use", label:"Private use"},
|
||||
{key:"commercial-use", label:"Commercial use"},
|
||||
{key:"modifications", label:"Modification"},
|
||||
{key:"distribution", label:"Distribution"},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > licenses/repository")
|
||||
return ({
|
||||
user:{
|
||||
repository:{
|
||||
licenseInfo:{spdxId:"MIT", name:"MIT License", nickname:null, key:"mit"},
|
||||
url:"https://github.com/lowlighter/metrics",
|
||||
databaseId:293860197,
|
||||
},
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > licenses/repository")
|
||||
return ({
|
||||
user:{
|
||||
repository:{
|
||||
licenseInfo:{spdxId:"MIT", name:"MIT License", nickname:null, key:"mit"},
|
||||
url:"https://github.com/lowlighter/metrics",
|
||||
databaseId:293860197,
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > notable/contributions")
|
||||
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > notable/contributions")
|
||||
return /after: "MOCKED_CURSOR"/m.test(query)
|
||||
? ({
|
||||
user:{
|
||||
repositoriesContributedTo:{
|
||||
edges:[],
|
||||
},
|
||||
},
|
||||
}) : ({
|
||||
})
|
||||
: ({
|
||||
user:{
|
||||
repositoriesContributedTo:{
|
||||
edges:[
|
||||
@@ -29,4 +31,4 @@
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > people/default")
|
||||
const type = query.match(/(?<type>followers|following)[(]/)?.groups?.type ?? "(unknown type)"
|
||||
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > people/default")
|
||||
const type = query.match(/(?<type>followers|following)[(]/)?.groups?.type ?? "(unknown type)"
|
||||
return /after: "MOCKED_CURSOR"/m.test(query)
|
||||
? ({
|
||||
user:{
|
||||
[type]:{
|
||||
edges:[],
|
||||
},
|
||||
},
|
||||
}) : ({
|
||||
})
|
||||
: ({
|
||||
user:{
|
||||
[type]:{
|
||||
edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map((login = faker.internet.userName()) => ({
|
||||
edges:new Array(Math.ceil(20 + 80 * Math.random())).fill(null).map((login = faker.internet.userName()) => ({
|
||||
cursor:"MOCKED_CURSOR",
|
||||
node:{
|
||||
login,
|
||||
@@ -21,4 +23,4 @@
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > people/repository")
|
||||
const type = query.match(/(?<type>stargazers|watchers)[(]/)?.groups?.type ?? "(unknown type)"
|
||||
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > people/repository")
|
||||
const type = query.match(/(?<type>stargazers|watchers)[(]/)?.groups?.type ?? "(unknown type)"
|
||||
return /after: "MOCKED_CURSOR"/m.test(query)
|
||||
? ({
|
||||
user:{
|
||||
repository:{
|
||||
[type]:{
|
||||
@@ -10,11 +11,12 @@
|
||||
},
|
||||
},
|
||||
},
|
||||
}) : ({
|
||||
})
|
||||
: ({
|
||||
user:{
|
||||
repository:{
|
||||
[type]:{
|
||||
edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map((login = faker.internet.userName()) => ({
|
||||
edges:new Array(Math.ceil(20 + 80 * Math.random())).fill(null).map((login = faker.internet.userName()) => ({
|
||||
cursor:"MOCKED_CURSOR",
|
||||
node:{
|
||||
login,
|
||||
@@ -25,4 +27,4 @@
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > people/sponsors")
|
||||
const type = query.match(/(?<type>sponsorshipsAsSponsor|sponsorshipsAsMaintainer)[(]/)?.groups?.type ?? "(unknown type)"
|
||||
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > people/sponsors")
|
||||
const type = query.match(/(?<type>sponsorshipsAsSponsor|sponsorshipsAsMaintainer)[(]/)?.groups?.type ?? "(unknown type)"
|
||||
return /after: "MOCKED_CURSOR"/m.test(query)
|
||||
? ({
|
||||
user:{
|
||||
login,
|
||||
[type]:{
|
||||
edges:[],
|
||||
},
|
||||
},
|
||||
}) : ({
|
||||
})
|
||||
: ({
|
||||
user:{
|
||||
login,
|
||||
[type]:{
|
||||
edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map((login = faker.internet.userName()) => ({
|
||||
edges:new Array(Math.ceil(20 + 80 * Math.random())).fill(null).map((login = faker.internet.userName()) => ({
|
||||
cursor:"MOCKED_CURSOR",
|
||||
node:{
|
||||
sponsorEntity:{
|
||||
@@ -29,4 +31,4 @@
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > projects/repository")
|
||||
return ({
|
||||
user:{
|
||||
repository:{
|
||||
project:{
|
||||
name:"Repository project example",
|
||||
updatedAt:`${faker.date.recent()}`,
|
||||
body:faker.lorem.paragraph(),
|
||||
progress:{
|
||||
doneCount:faker.datatype.number(10),
|
||||
inProgressCount:faker.datatype.number(10),
|
||||
todoCount:faker.datatype.number(10),
|
||||
enabled:true,
|
||||
},
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > projects/repository")
|
||||
return ({
|
||||
user:{
|
||||
repository:{
|
||||
project:{
|
||||
name:"Repository project example",
|
||||
updatedAt:`${faker.date.recent()}`,
|
||||
body:faker.lorem.paragraph(),
|
||||
progress:{
|
||||
doneCount:faker.datatype.number(10),
|
||||
inProgressCount:faker.datatype.number(10),
|
||||
todoCount:faker.datatype.number(10),
|
||||
enabled:true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > projects/user")
|
||||
return ({
|
||||
user:{
|
||||
projects:{
|
||||
totalCount:1,
|
||||
nodes:[
|
||||
{
|
||||
name:"User-owned project",
|
||||
updatedAt:`${faker.date.recent()}`,
|
||||
body:faker.lorem.paragraph(),
|
||||
progress:{
|
||||
doneCount:faker.datatype.number(10),
|
||||
inProgressCount:faker.datatype.number(10),
|
||||
todoCount:faker.datatype.number(10),
|
||||
enabled:true,
|
||||
},
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > projects/user")
|
||||
return ({
|
||||
user:{
|
||||
projects:{
|
||||
totalCount:1,
|
||||
nodes:[
|
||||
{
|
||||
name:"User-owned project",
|
||||
updatedAt:`${faker.date.recent()}`,
|
||||
body:faker.lorem.paragraph(),
|
||||
progress:{
|
||||
doneCount:faker.datatype.number(10),
|
||||
inProgressCount:faker.datatype.number(10),
|
||||
todoCount:faker.datatype.number(10),
|
||||
enabled:true,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > reactions/default")
|
||||
const type = query.match(/(?<type>issues|issueComments)[(]/)?.groups?.type ?? "(unknown type)"
|
||||
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > reactions/default")
|
||||
const type = query.match(/(?<type>issues|issueComments)[(]/)?.groups?.type ?? "(unknown type)"
|
||||
return /after: "MOCKED_CURSOR"/m.test(query)
|
||||
? ({
|
||||
user:{
|
||||
[type]:{
|
||||
edges:[],
|
||||
nodes:[],
|
||||
},
|
||||
},
|
||||
}) : ({
|
||||
})
|
||||
: ({
|
||||
user:{
|
||||
[type]:{
|
||||
edges:new Array(100).fill(null).map(_ => ({
|
||||
@@ -27,4 +29,4 @@
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > stargazers/default")
|
||||
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > stargazers/default")
|
||||
return /after: "MOCKED_CURSOR"/m.test(query)
|
||||
? ({
|
||||
repository:{
|
||||
stargazers:{
|
||||
edges:[],
|
||||
},
|
||||
},
|
||||
}) : ({
|
||||
})
|
||||
: ({
|
||||
repository:{
|
||||
stargazers:{
|
||||
edges:new Array(faker.datatype.number({min:50, max:100})).fill(null).map(() => ({
|
||||
@@ -17,4 +19,4 @@
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
/**Mocked data */
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > stars/default")
|
||||
return ({
|
||||
user:{
|
||||
starredRepositories:{
|
||||
edges:[
|
||||
{
|
||||
starredAt:`${faker.date.recent(14)}`,
|
||||
node:{
|
||||
description:"📊 An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !",
|
||||
forkCount:faker.datatype.number(100),
|
||||
isFork:false,
|
||||
issues:{
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
nameWithOwner:"lowlighter/metrics",
|
||||
openGraphImageUrl:"https://repository-images.githubusercontent.com/293860197/7fd72080-496d-11eb-8fe0-238b38a0746a",
|
||||
pullRequests:{
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
stargazerCount:faker.datatype.number(10000),
|
||||
licenseInfo:{
|
||||
nickname:null,
|
||||
name:"MIT License",
|
||||
},
|
||||
primaryLanguage:{
|
||||
color:"#f1e05a",
|
||||
name:"JavaScript",
|
||||
},
|
||||
export default function({faker, query, login = faker.internet.userName()}) {
|
||||
console.debug("metrics/compute/mocks > mocking graphql api result > stars/default")
|
||||
return ({
|
||||
user:{
|
||||
starredRepositories:{
|
||||
edges:[
|
||||
{
|
||||
starredAt:`${faker.date.recent(14)}`,
|
||||
node:{
|
||||
description:"📊 An image generator with 20+ metrics about your GitHub account such as activity, community, repositories, coding habits, website performances, music played, starred topics, etc. that you can put on your profile or elsewhere !",
|
||||
forkCount:faker.datatype.number(100),
|
||||
isFork:false,
|
||||
issues:{
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
nameWithOwner:"lowlighter/metrics",
|
||||
openGraphImageUrl:"https://repository-images.githubusercontent.com/293860197/7fd72080-496d-11eb-8fe0-238b38a0746a",
|
||||
pullRequests:{
|
||||
totalCount:faker.datatype.number(100),
|
||||
},
|
||||
stargazerCount:faker.datatype.number(10000),
|
||||
licenseInfo:{
|
||||
nickname:null,
|
||||
name:"MIT License",
|
||||
},
|
||||
primaryLanguage:{
|
||||
color:"#f1e05a",
|
||||
name:"JavaScript",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,341 +1,341 @@
|
||||
/**Mocked data */
|
||||
export default function({faker}, target, that, [{username:login, page, per_page}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.activity.listEventsForAuthenticatedUser")
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/users/${login}/events?per_page=${per_page}&page=${page}`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
export default function({faker}, target, that, [{username:login, page, per_page}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.activity.listEventsForAuthenticatedUser")
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/users/${login}/events?per_page=${per_page}&page=${page}`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:page < 1 ? [] : [
|
||||
{
|
||||
id:"10000000000",
|
||||
type:"CommitCommentEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
comment:{
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
path:faker.system.fileName(),
|
||||
commit_id:"MOCKED_SHA",
|
||||
body:faker.lorem.sentence(),
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
data:page < 1 ? [] : [
|
||||
{
|
||||
id:"10000000000",
|
||||
type:"CommitCommentEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
comment:{
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
path:faker.system.fileName(),
|
||||
commit_id:"MOCKED_SHA",
|
||||
body:faker.lorem.sentence(),
|
||||
{
|
||||
id:"10000000001",
|
||||
type:"PullRequestReviewCommentEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:"created",
|
||||
comment:{
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
body:faker.lorem.paragraph(),
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000001",
|
||||
type:"PullRequestReviewCommentEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:"created",
|
||||
comment:{
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
body:faker.lorem.paragraph(),
|
||||
},
|
||||
pull_request:{
|
||||
title:faker.lorem.sentence(),
|
||||
number:1,
|
||||
user:{
|
||||
login:faker.internet.userName(),
|
||||
},
|
||||
body:"",
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000002",
|
||||
type:"IssuesEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:faker.random.arrayElement(["opened", "closed", "reopened"]),
|
||||
issue:{
|
||||
number:2,
|
||||
title:faker.lorem.sentence(),
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
body:faker.lorem.paragraph(),
|
||||
performed_via_github_app:null,
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000003",
|
||||
type:"GollumEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
pages:[
|
||||
{
|
||||
page_name:faker.lorem.sentence(),
|
||||
title:faker.lorem.sentence(),
|
||||
summary:null,
|
||||
action:"created",
|
||||
sha:"MOCKED_SHA",
|
||||
},
|
||||
],
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000004",
|
||||
type:"IssueCommentEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:"created",
|
||||
issue:{
|
||||
number:3,
|
||||
title:faker.lorem.sentence(),
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
labels:[
|
||||
{
|
||||
name:"lorem ipsum",
|
||||
color:"d876e3",
|
||||
},
|
||||
],
|
||||
state:"open",
|
||||
},
|
||||
comment:{
|
||||
body:faker.lorem.paragraph(),
|
||||
performed_via_github_app:null,
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000005",
|
||||
type:"ForkEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
forkee:{
|
||||
name:faker.random.word(),
|
||||
full_name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000006",
|
||||
type:"PullRequestReviewEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:"created",
|
||||
review:{
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
state:"approved",
|
||||
},
|
||||
pull_request:{
|
||||
state:"open",
|
||||
number:4,
|
||||
locked:false,
|
||||
title:faker.lorem.sentence(),
|
||||
user:{
|
||||
login:faker.internet.userName(),
|
||||
},
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000007",
|
||||
type:"ReleaseEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:"published",
|
||||
release:{
|
||||
tag_name:`v${faker.datatype.number()}.${faker.datatype.number()}`,
|
||||
name:faker.random.words(4),
|
||||
draft:faker.datatype.boolean(),
|
||||
prerelease:faker.datatype.boolean(),
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000008",
|
||||
type:"CreateEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
ref:faker.lorem.slug(),
|
||||
ref_type:faker.random.arrayElement(["tag", "branch"]),
|
||||
master_branch:"master",
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"100000000009",
|
||||
type:"WatchEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:"lowlighter/metrics",
|
||||
},
|
||||
payload:{action:"started"},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000010",
|
||||
type:"DeleteEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
ref:faker.lorem.slug(),
|
||||
ref_type:faker.random.arrayElement(["tag", "branch"]),
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000011",
|
||||
type:"PushEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
size:1,
|
||||
ref:"refs/heads/master",
|
||||
commits:[
|
||||
{
|
||||
sha:"MOCKED_SHA",
|
||||
message:faker.lorem.sentence(),
|
||||
url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA",
|
||||
},
|
||||
],
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000012",
|
||||
type:"PullRequestEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:faker.random.arrayElement(["opened", "closed"]),
|
||||
number:5,
|
||||
pull_request:{
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
state:"open",
|
||||
title:faker.lorem.sentence(),
|
||||
additions:faker.datatype.number(1000),
|
||||
deletions:faker.datatype.number(1000),
|
||||
changed_files:faker.datatype.number(10),
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000013",
|
||||
type:"MemberEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
member:{
|
||||
pull_request:{
|
||||
title:faker.lorem.sentence(),
|
||||
number:1,
|
||||
user:{
|
||||
login:faker.internet.userName(),
|
||||
},
|
||||
action:"added",
|
||||
body:"",
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000014",
|
||||
type:"PublicEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000002",
|
||||
type:"IssuesEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:faker.random.arrayElement(["opened", "closed", "reopened"]),
|
||||
issue:{
|
||||
number:2,
|
||||
title:faker.lorem.sentence(),
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
body:faker.lorem.paragraph(),
|
||||
performed_via_github_app:null,
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000003",
|
||||
type:"GollumEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
pages:[
|
||||
{
|
||||
page_name:faker.lorem.sentence(),
|
||||
title:faker.lorem.sentence(),
|
||||
summary:null,
|
||||
action:"created",
|
||||
sha:"MOCKED_SHA",
|
||||
},
|
||||
],
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000004",
|
||||
type:"IssueCommentEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:"created",
|
||||
issue:{
|
||||
number:3,
|
||||
title:faker.lorem.sentence(),
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
labels:[
|
||||
{
|
||||
name:"lorem ipsum",
|
||||
color:"d876e3",
|
||||
},
|
||||
],
|
||||
state:"open",
|
||||
},
|
||||
comment:{
|
||||
body:faker.lorem.paragraph(),
|
||||
performed_via_github_app:null,
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000005",
|
||||
type:"ForkEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
forkee:{
|
||||
name:faker.random.word(),
|
||||
full_name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000006",
|
||||
type:"PullRequestReviewEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:"created",
|
||||
review:{
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
state:"approved",
|
||||
},
|
||||
pull_request:{
|
||||
state:"open",
|
||||
number:4,
|
||||
locked:false,
|
||||
title:faker.lorem.sentence(),
|
||||
user:{
|
||||
login:faker.internet.userName(),
|
||||
},
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000007",
|
||||
type:"ReleaseEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:"published",
|
||||
release:{
|
||||
tag_name:`v${faker.datatype.number()}.${faker.datatype.number()}`,
|
||||
name:faker.random.words(4),
|
||||
draft:faker.datatype.boolean(),
|
||||
prerelease:faker.datatype.boolean(),
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000008",
|
||||
type:"CreateEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
ref:faker.lorem.slug(),
|
||||
ref_type:faker.random.arrayElement(["tag", "branch"]),
|
||||
master_branch:"master",
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"100000000009",
|
||||
type:"WatchEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:"lowlighter/metrics",
|
||||
},
|
||||
payload:{action:"started"},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000010",
|
||||
type:"DeleteEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
ref:faker.lorem.slug(),
|
||||
ref_type:faker.random.arrayElement(["tag", "branch"]),
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000011",
|
||||
type:"PushEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
size:1,
|
||||
ref:"refs/heads/master",
|
||||
commits:[
|
||||
{
|
||||
sha:"MOCKED_SHA",
|
||||
message:faker.lorem.sentence(),
|
||||
url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA",
|
||||
},
|
||||
],
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000012",
|
||||
type:"PullRequestEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
action:faker.random.arrayElement(["opened", "closed"]),
|
||||
number:5,
|
||||
pull_request:{
|
||||
user:{
|
||||
login,
|
||||
},
|
||||
state:"open",
|
||||
title:faker.lorem.sentence(),
|
||||
additions:faker.datatype.number(1000),
|
||||
deletions:faker.datatype.number(1000),
|
||||
changed_files:faker.datatype.number(10),
|
||||
},
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000013",
|
||||
type:"MemberEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{
|
||||
member:{
|
||||
login:faker.internet.userName(),
|
||||
},
|
||||
action:"added",
|
||||
},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
{
|
||||
id:"10000000014",
|
||||
type:"PublicEvent",
|
||||
actor:{
|
||||
login,
|
||||
},
|
||||
repo:{
|
||||
name:`${faker.random.word()}/${faker.random.word()}`,
|
||||
},
|
||||
payload:{},
|
||||
created_at:faker.date.recent(7),
|
||||
public:true,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//Imports
|
||||
import listEventsForAuthenticatedUser from "./listEventsForAuthenticatedUser.mjs"
|
||||
import listEventsForAuthenticatedUser from "./listEventsForAuthenticatedUser.mjs"
|
||||
|
||||
/**Mocked data */
|
||||
export default function({faker}, target, that, [{username:login, page, per_page}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.activity.listRepoEvents")
|
||||
return listEventsForAuthenticatedUser(...arguments)
|
||||
}
|
||||
export default function({faker}, target, that, [{username:login, page, per_page}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.activity.listRepoEvents")
|
||||
return listEventsForAuthenticatedUser(...arguments)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,23 @@
|
||||
/**Mocked data */
|
||||
export default function({faker}, target, that, args) {
|
||||
return ({
|
||||
status:200,
|
||||
url:"https://api.github.com/rate_limit",
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
export default function({faker}, target, that, args) {
|
||||
return ({
|
||||
status:200,
|
||||
url:"https://api.github.com/rate_limit",
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:{
|
||||
resources:{
|
||||
core:{limit:5000, used:0, remaining:5000, reset:0},
|
||||
search:{limit:30, used:0, remaining:30, reset:0},
|
||||
graphql:{limit:5000, used:0, remaining:5000, reset:0},
|
||||
integration_manifest:{limit:5000, used:0, remaining:5000, reset:0},
|
||||
source_import:{limit:100, used:0, remaining:100, reset:0},
|
||||
code_scanning_upload:{limit:500, used:0, remaining:500, reset:0},
|
||||
},
|
||||
data:{
|
||||
resources:{
|
||||
core:{limit:5000, used:0, remaining:5000, reset:0},
|
||||
search:{limit:30, used:0, remaining:30, reset:0},
|
||||
graphql:{limit:5000, used:0, remaining:5000, reset:0},
|
||||
integration_manifest:{limit:5000, used:0, remaining:5000, reset:0},
|
||||
source_import:{limit:100, used:0, remaining:100, reset:0},
|
||||
code_scanning_upload:{limit:500, used:0, remaining:500, reset:0},
|
||||
},
|
||||
rate:{limit:5000, used:0, remaining:"MOCKED", reset:0},
|
||||
},
|
||||
})
|
||||
}
|
||||
rate:{limit:5000, used:0, remaining:"MOCKED", reset:0},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
/**Mocked data */
|
||||
export default function({faker}, target, that, [{owner, repo}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getContributorsStats")
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/repos/${owner}/${repo}/stats/contributors`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:[
|
||||
{
|
||||
total:faker.datatype.number(10000),
|
||||
weeks:[
|
||||
{w:1, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
|
||||
{w:2, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
|
||||
{w:3, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
|
||||
{w:4, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
|
||||
],
|
||||
author:{
|
||||
login:owner,
|
||||
},
|
||||
export default function({faker}, target, that, [{owner, repo}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getContributorsStats")
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/repos/${owner}/${repo}/stats/contributors`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:[
|
||||
{
|
||||
total:faker.datatype.number(10000),
|
||||
weeks:[
|
||||
{w:1, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
|
||||
{w:2, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
|
||||
{w:3, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
|
||||
{w:4, a:faker.datatype.number(10000), d:faker.datatype.number(10000), c:faker.datatype.number(10000)},
|
||||
],
|
||||
author:{
|
||||
login:owner,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
/**Mocked data */
|
||||
export default function({faker}, target, that, [{owner, repo}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getViews")
|
||||
const count = faker.datatype.number(10000)*2
|
||||
const uniques = faker.datatype.number(count)*2
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/repos/${owner}/${repo}/traffic/views`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:{
|
||||
count,
|
||||
uniques,
|
||||
views:[
|
||||
{timestamp:`${faker.date.recent()}`, count:count/2, uniques:uniques/2},
|
||||
{timestamp:`${faker.date.recent()}`, count:count/2, uniques:uniques/2},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
export default function({faker}, target, that, [{owner, repo}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getViews")
|
||||
const count = faker.datatype.number(10000) * 2
|
||||
const uniques = faker.datatype.number(count) * 2
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/repos/${owner}/${repo}/traffic/views`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:{
|
||||
count,
|
||||
uniques,
|
||||
views:[
|
||||
{timestamp:`${faker.date.recent()}`, count:count / 2, uniques:uniques / 2},
|
||||
{timestamp:`${faker.date.recent()}`, count:count / 2, uniques:uniques / 2},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
/**Mocked data */
|
||||
export default function({faker}, target, that, [{page, per_page, owner, repo}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.listCommits")
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/repos/${owner}/${repo}/commits?per_page=${per_page}&page=${page}`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:page < 2 ? new Array(per_page).fill(null).map(() => ({
|
||||
sha:"MOCKED_SHA",
|
||||
get author() {
|
||||
return this.commit.author
|
||||
export default function({faker}, target, that, [{page, per_page, owner, repo}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.listCommits")
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/repos/${owner}/${repo}/commits?per_page=${per_page}&page=${page}`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:page < 2
|
||||
? new Array(per_page).fill(null).map(() => ({
|
||||
sha:"MOCKED_SHA",
|
||||
get author() {
|
||||
return this.commit.author
|
||||
},
|
||||
commit:{
|
||||
message:faker.lorem.sentence(),
|
||||
author:{
|
||||
name:owner,
|
||||
login:faker.internet.userName(),
|
||||
avatar_url:null,
|
||||
date:`${faker.date.recent(14)}`,
|
||||
},
|
||||
commit:{
|
||||
message:faker.lorem.sentence(),
|
||||
author:{
|
||||
name:owner,
|
||||
login:faker.internet.userName(),
|
||||
avatar_url:null,
|
||||
date:`${faker.date.recent(14)}`,
|
||||
},
|
||||
committer:{
|
||||
name:owner,
|
||||
login:faker.internet.userName(),
|
||||
avatar_url:null,
|
||||
date:`${faker.date.recent(14)}`,
|
||||
},
|
||||
committer:{
|
||||
name:owner,
|
||||
login:faker.internet.userName(),
|
||||
avatar_url:null,
|
||||
date:`${faker.date.recent(14)}`,
|
||||
},
|
||||
})) : [],
|
||||
})
|
||||
}
|
||||
},
|
||||
}))
|
||||
: [],
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/**Mocked data */
|
||||
export default function({faker}, target, that, [{owner, repo}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.listContributors")
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/repos/${owner}/${repo}/contributors`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:new Array(40+faker.datatype.number(60)).fill(null).map(() => ({
|
||||
login:faker.internet.userName(),
|
||||
avatar_url:null,
|
||||
contributions:faker.datatype.number(1000),
|
||||
})),
|
||||
})
|
||||
}
|
||||
export default function({faker}, target, that, [{owner, repo}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.listContributors")
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/repos/${owner}/${repo}/contributors`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:new Array(40 + faker.datatype.number(60)).fill(null).map(() => ({
|
||||
login:faker.internet.userName(),
|
||||
avatar_url:null,
|
||||
contributions:faker.datatype.number(1000),
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,59 +1,59 @@
|
||||
/**Mocked data */
|
||||
export default function({faker}, target, that, args) {
|
||||
//Arguments
|
||||
const [url] = args
|
||||
//Head request
|
||||
if (/^HEAD .$/.test(url)) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.request HEAD")
|
||||
return ({
|
||||
status:200,
|
||||
url:"https://api.github.com/",
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:undefined,
|
||||
})
|
||||
}
|
||||
//Commit content
|
||||
if (/^https:..api.github.com.repos.lowlighter.metrics.commits.MOCKED_SHA/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking rest api result > rest.request ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA",
|
||||
data:{
|
||||
sha:"MOCKED_SHA",
|
||||
commit:{
|
||||
author:{
|
||||
name:faker.internet.userName(),
|
||||
email:faker.internet.email(),
|
||||
date:`${faker.date.recent(7)}`,
|
||||
},
|
||||
committer:{
|
||||
name:faker.internet.userName(),
|
||||
email:faker.internet.email(),
|
||||
date:`${faker.date.recent(7)}`,
|
||||
},
|
||||
},
|
||||
author:{
|
||||
login:faker.internet.userName(),
|
||||
id:faker.datatype.number(100000000),
|
||||
},
|
||||
committer:{
|
||||
login:faker.internet.userName(),
|
||||
id:faker.datatype.number(100000000),
|
||||
},
|
||||
files:[
|
||||
{
|
||||
sha:"MOCKED_SHA",
|
||||
filename:faker.system.fileName(),
|
||||
patch:"@@ -0,0 +1,5 @@\n+//Imports\n+ import app from \"./src/app.mjs\"\n+\n+//Start app\n+ await app()\n\\ No newline at end of file",
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return target(...args)
|
||||
export default function({faker}, target, that, args) {
|
||||
//Arguments
|
||||
const [url] = args
|
||||
//Head request
|
||||
if (/^HEAD .$/.test(url)) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.request HEAD")
|
||||
return ({
|
||||
status:200,
|
||||
url:"https://api.github.com/",
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:undefined,
|
||||
})
|
||||
}
|
||||
//Commit content
|
||||
if (/^https:..api.github.com.repos.lowlighter.metrics.commits.MOCKED_SHA/.test(url)) {
|
||||
console.debug(`metrics/compute/mocks > mocking rest api result > rest.request ${url}`)
|
||||
return ({
|
||||
status:200,
|
||||
url:"https://api.github.com/repos/lowlighter/metrics/commits/MOCKED_SHA",
|
||||
data:{
|
||||
sha:"MOCKED_SHA",
|
||||
commit:{
|
||||
author:{
|
||||
name:faker.internet.userName(),
|
||||
email:faker.internet.email(),
|
||||
date:`${faker.date.recent(7)}`,
|
||||
},
|
||||
committer:{
|
||||
name:faker.internet.userName(),
|
||||
email:faker.internet.email(),
|
||||
date:`${faker.date.recent(7)}`,
|
||||
},
|
||||
},
|
||||
author:{
|
||||
login:faker.internet.userName(),
|
||||
id:faker.datatype.number(100000000),
|
||||
},
|
||||
committer:{
|
||||
login:faker.internet.userName(),
|
||||
id:faker.datatype.number(100000000),
|
||||
},
|
||||
files:[
|
||||
{
|
||||
sha:"MOCKED_SHA",
|
||||
filename:faker.system.fileName(),
|
||||
patch:'@@ -0,0 +1,5 @@\n+//Imports\n+ import app from "./src/app.mjs"\n+\n+//Start app\n+ await app()\n\\ No newline at end of file',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return target(...args)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
/**Mocked data */
|
||||
export default function({faker}, target, that, [{username}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getByUsername")
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/users/${username}/`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:{
|
||||
login:faker.internet.userName(),
|
||||
avatar_url:null,
|
||||
contributions:faker.datatype.number(1000),
|
||||
},
|
||||
})
|
||||
}
|
||||
export default function({faker}, target, that, [{username}]) {
|
||||
console.debug("metrics/compute/mocks > mocking rest api result > rest.repos.getByUsername")
|
||||
return ({
|
||||
status:200,
|
||||
url:`https://api.github.com/users/${username}/`,
|
||||
headers:{
|
||||
server:"GitHub.com",
|
||||
status:"200 OK",
|
||||
"x-oauth-scopes":"repo",
|
||||
},
|
||||
data:{
|
||||
login:faker.internet.userName(),
|
||||
avatar_url:null,
|
||||
contributions:faker.datatype.number(1000),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,150 +1,150 @@
|
||||
//Imports
|
||||
import axios from "axios"
|
||||
import faker from "faker"
|
||||
import paths from "path"
|
||||
import urls from "url"
|
||||
import rss from "rss-parser"
|
||||
import fs from "fs/promises"
|
||||
import fs from "fs/promises"
|
||||
import axios from "axios"
|
||||
import faker from "faker"
|
||||
import paths from "path"
|
||||
import rss from "rss-parser"
|
||||
import urls from "url"
|
||||
|
||||
//Mocked state
|
||||
let mocked = false
|
||||
let mocked = false
|
||||
|
||||
//Mocking
|
||||
export default async function({graphql, rest}) {
|
||||
export default async function({graphql, rest}) {
|
||||
//Check if already mocked
|
||||
if (mocked)
|
||||
return {graphql, rest}
|
||||
mocked = true
|
||||
console.debug("metrics/compute/mocks > mocking")
|
||||
|
||||
//Check if already mocked
|
||||
if (mocked)
|
||||
return {graphql, rest}
|
||||
mocked = true
|
||||
console.debug("metrics/compute/mocks > mocking")
|
||||
//Load mocks
|
||||
const __mocks = paths.join(paths.dirname(urls.fileURLToPath(import.meta.url)))
|
||||
const mock = async ({directory, mocks}) => {
|
||||
for (const entry of await fs.readdir(directory)) {
|
||||
if ((await fs.lstat(paths.join(directory, entry))).isDirectory()) {
|
||||
if (!mocks[entry])
|
||||
mocks[entry] = {}
|
||||
await mock({directory:paths.join(directory, entry), mocks:mocks[entry]})
|
||||
}
|
||||
else
|
||||
mocks[entry.replace(/[.]mjs$/, "")] = (await import(urls.pathToFileURL(paths.join(directory, entry)).href)).default
|
||||
|
||||
//Load mocks
|
||||
const __mocks = paths.join(paths.dirname(urls.fileURLToPath(import.meta.url)))
|
||||
const mock = async({directory, mocks}) => {
|
||||
for (const entry of await fs.readdir(directory)) {
|
||||
if ((await fs.lstat(paths.join(directory, entry))).isDirectory()) {
|
||||
if (!mocks[entry])
|
||||
mocks[entry] = {}
|
||||
await mock({directory:paths.join(directory, entry), mocks:mocks[entry]})
|
||||
}
|
||||
else
|
||||
mocks[entry.replace(/[.]mjs$/, "")] = (await import(urls.pathToFileURL(paths.join(directory, entry)).href)).default
|
||||
}
|
||||
return mocks
|
||||
}
|
||||
const mocks = await mock({directory:paths.join(__mocks, "api"), mocks:{}})
|
||||
|
||||
//GraphQL API mocking
|
||||
{
|
||||
//Unmocked
|
||||
console.debug("metrics/compute/mocks > mocking graphql api")
|
||||
const unmocked = graphql
|
||||
//Mocked
|
||||
graphql = new Proxy(unmocked, {
|
||||
apply(target, that, args) {
|
||||
//Arguments
|
||||
const [query] = args
|
||||
const login = query.match(/login: "(?<login>.*?)"/)?.groups?.login ?? faker.internet.userName()
|
||||
|
||||
//Search for mocked query
|
||||
for (const mocked of Object.keys(mocks.github.graphql)) {
|
||||
if (new RegExp(`^query ${mocked.replace(/([.]\w)/g, (_, g) => g.toLocaleUpperCase().substring(1)).replace(/^(\w)/g, (_, g) => g.toLocaleUpperCase())} `).test(query))
|
||||
return mocks.github.graphql[mocked]({faker, query, login})
|
||||
}
|
||||
return mocks
|
||||
}
|
||||
const mocks = await mock({directory:paths.join(__mocks, "api"), mocks:{}})
|
||||
|
||||
//GraphQL API mocking
|
||||
{
|
||||
//Unmocked
|
||||
console.debug("metrics/compute/mocks > mocking graphql api")
|
||||
const unmocked = graphql
|
||||
//Mocked
|
||||
graphql = new Proxy(unmocked, {
|
||||
apply(target, that, args) {
|
||||
//Arguments
|
||||
const [query] = args
|
||||
const login = query.match(/login: "(?<login>.*?)"/)?.groups?.login ?? faker.internet.userName()
|
||||
|
||||
//Search for mocked query
|
||||
for (const mocked of Object.keys(mocks.github.graphql)) {
|
||||
if (new RegExp(`^query ${mocked.replace(/([.]\w)/g, (_, g) => g.toLocaleUpperCase().substring(1)).replace(/^(\w)/g, (_, g) => g.toLocaleUpperCase())} `).test(query))
|
||||
return mocks.github.graphql[mocked]({faker, query, login})
|
||||
}
|
||||
|
||||
//Unmocked call
|
||||
return target(...args)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
//Rest API mocking
|
||||
{
|
||||
//Unmocked
|
||||
console.debug("metrics/compute/mocks > mocking rest api")
|
||||
const unmocked = {}
|
||||
//Mocked
|
||||
const mocker = ({path = "rest", mocks, mocked}) => {
|
||||
for (const [key, value] of Object.entries(mocks)) {
|
||||
console.debug(`metrics/compute/mocks > mocking rest api > mocking ${path}.${key}`)
|
||||
if (typeof value === "function") {
|
||||
unmocked[path] = value
|
||||
mocked[key] = new Proxy(unmocked[path], {apply:value.bind(null, {faker})})
|
||||
}
|
||||
else
|
||||
mocker({path:`${path}.${key}`, mocks:mocks[key], mocked:mocked[key]})
|
||||
}
|
||||
}
|
||||
mocker({mocks:mocks.github.rest, mocked:rest})
|
||||
}
|
||||
|
||||
//Axios mocking
|
||||
{
|
||||
//Unmocked
|
||||
console.debug("metrics/compute/mocks > mocking axios")
|
||||
const unmocked = {get:axios.get, post:axios.post}
|
||||
|
||||
//Mocked post requests
|
||||
axios.post = new Proxy(unmocked.post, {
|
||||
apply(target, that, args) {
|
||||
//Arguments
|
||||
const [url, body] = args
|
||||
|
||||
//Search for mocked request
|
||||
for (const service of Object.keys(mocks.axios.post)) {
|
||||
const mocked = mocks.axios.post[service]({faker, url, body})
|
||||
if (mocked)
|
||||
return mocked
|
||||
}
|
||||
|
||||
//Unmocked call
|
||||
return target(...args)
|
||||
},
|
||||
})
|
||||
|
||||
//Mocked get requests
|
||||
axios.get = new Proxy(unmocked.get, {
|
||||
apply(target, that, args) {
|
||||
//Arguments
|
||||
const [url, options] = args
|
||||
|
||||
//Search for mocked request
|
||||
for (const service of Object.keys(mocks.axios.get)) {
|
||||
const mocked = mocks.axios.get[service]({faker, url, options})
|
||||
if (mocked)
|
||||
return mocked
|
||||
}
|
||||
|
||||
//Unmocked call
|
||||
return target(...args)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
//RSS mocking
|
||||
{
|
||||
//Unmocked
|
||||
console.debug("metrics/compute/mocks > mocking rss-parser")
|
||||
|
||||
//Mock rss feed
|
||||
rss.prototype.parseURL = function(url) {
|
||||
console.debug(`metrics/compute/mocks > mocking rss feed result > ${url}`)
|
||||
return ({
|
||||
items:new Array(30).fill(null).map(_ => ({
|
||||
title:faker.lorem.sentence(),
|
||||
link:faker.internet.url(),
|
||||
content:faker.lorem.paragraphs(),
|
||||
contentSnippet:faker.lorem.paragraph(),
|
||||
isoDate:faker.date.recent(),
|
||||
})),
|
||||
title:faker.lorem.words(),
|
||||
description:faker.lorem.paragraph(),
|
||||
link:url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//Return mocked elements
|
||||
return {graphql, rest}
|
||||
//Unmocked call
|
||||
return target(...args)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
//Rest API mocking
|
||||
{
|
||||
//Unmocked
|
||||
console.debug("metrics/compute/mocks > mocking rest api")
|
||||
const unmocked = {}
|
||||
//Mocked
|
||||
const mocker = ({path = "rest", mocks, mocked}) => {
|
||||
for (const [key, value] of Object.entries(mocks)) {
|
||||
console.debug(`metrics/compute/mocks > mocking rest api > mocking ${path}.${key}`)
|
||||
if (typeof value === "function") {
|
||||
unmocked[path] = value
|
||||
mocked[key] = new Proxy(unmocked[path], {apply:value.bind(null, {faker})})
|
||||
}
|
||||
else
|
||||
mocker({path:`${path}.${key}`, mocks:mocks[key], mocked:mocked[key]})
|
||||
|
||||
}
|
||||
}
|
||||
mocker({mocks:mocks.github.rest, mocked:rest})
|
||||
}
|
||||
|
||||
//Axios mocking
|
||||
{
|
||||
//Unmocked
|
||||
console.debug("metrics/compute/mocks > mocking axios")
|
||||
const unmocked = {get:axios.get, post:axios.post}
|
||||
|
||||
//Mocked post requests
|
||||
axios.post = new Proxy(unmocked.post, {
|
||||
apply(target, that, args) {
|
||||
//Arguments
|
||||
const [url, body] = args
|
||||
|
||||
//Search for mocked request
|
||||
for (const service of Object.keys(mocks.axios.post)) {
|
||||
const mocked = mocks.axios.post[service]({faker, url, body})
|
||||
if (mocked)
|
||||
return mocked
|
||||
}
|
||||
|
||||
//Unmocked call
|
||||
return target(...args)
|
||||
},
|
||||
})
|
||||
|
||||
//Mocked get requests
|
||||
axios.get = new Proxy(unmocked.get, {
|
||||
apply(target, that, args) {
|
||||
//Arguments
|
||||
const [url, options] = args
|
||||
|
||||
//Search for mocked request
|
||||
for (const service of Object.keys(mocks.axios.get)) {
|
||||
const mocked = mocks.axios.get[service]({faker, url, options})
|
||||
if (mocked)
|
||||
return mocked
|
||||
}
|
||||
|
||||
//Unmocked call
|
||||
return target(...args)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
//RSS mocking
|
||||
{
|
||||
//Unmocked
|
||||
console.debug("metrics/compute/mocks > mocking rss-parser")
|
||||
|
||||
//Mock rss feed
|
||||
rss.prototype.parseURL = function(url) {
|
||||
console.debug(`metrics/compute/mocks > mocking rss feed result > ${url}`)
|
||||
return ({
|
||||
items:new Array(30).fill(null).map(_ => ({
|
||||
title:faker.lorem.sentence(),
|
||||
link:faker.internet.url(),
|
||||
content:faker.lorem.paragraphs(),
|
||||
contentSnippet:faker.lorem.paragraph(),
|
||||
isoDate:faker.date.recent(),
|
||||
})),
|
||||
title:faker.lorem.words(),
|
||||
description:faker.lorem.paragraph(),
|
||||
link:url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//Return mocked elements
|
||||
return {graphql, rest}
|
||||
}
|
||||
|
||||
@@ -1,321 +1,338 @@
|
||||
//Imports
|
||||
import octokit from "@octokit/graphql"
|
||||
import OctokitRest from "@octokit/rest"
|
||||
import express from "express"
|
||||
import ratelimit from "express-rate-limit"
|
||||
import compression from "compression"
|
||||
import cache from "memory-cache"
|
||||
import util from "util"
|
||||
import setup from "../metrics/setup.mjs"
|
||||
import mocks from "../mocks/index.mjs"
|
||||
import metrics from "../metrics/index.mjs"
|
||||
import octokit from "@octokit/graphql"
|
||||
import OctokitRest from "@octokit/rest"
|
||||
import compression from "compression"
|
||||
import express from "express"
|
||||
import ratelimit from "express-rate-limit"
|
||||
import cache from "memory-cache"
|
||||
import util from "util"
|
||||
import metrics from "../metrics/index.mjs"
|
||||
import setup from "../metrics/setup.mjs"
|
||||
import mocks from "../mocks/index.mjs"
|
||||
|
||||
/**App */
|
||||
export default async function({mock, nosettings} = {}) {
|
||||
export default async function({mock, nosettings} = {}) {
|
||||
//Load configuration settings
|
||||
const {conf, Plugins, Templates} = await setup({nosettings})
|
||||
const {token, maxusers = 0, restricted = [], debug = false, cached = 30 * 60 * 1000, port = 3000, ratelimiter = null, plugins = null} = conf.settings
|
||||
mock = mock || conf.settings.mocked
|
||||
|
||||
//Load configuration settings
|
||||
const {conf, Plugins, Templates} = await setup({nosettings})
|
||||
const {token, maxusers = 0, restricted = [], debug = false, cached = 30*60*1000, port = 3000, ratelimiter = null, plugins = null} = conf.settings
|
||||
mock = mock || conf.settings.mocked
|
||||
|
||||
//Process mocking and default plugin state
|
||||
for (const plugin of Object.keys(Plugins).filter(x => !["base", "core"].includes(x))) {
|
||||
//Initialization
|
||||
const {settings} = conf
|
||||
if (!settings.plugins[plugin])
|
||||
settings.plugins[plugin] = {}
|
||||
//Auto-enable plugin if needed
|
||||
if (conf.settings["plugins.default"])
|
||||
settings.plugins[plugin].enabled = settings.plugins[plugin].enabled ?? (console.debug(`metrics/app > auto-enabling ${plugin}`), true)
|
||||
//Mock plugins tokens if they're undefined
|
||||
if (mock) {
|
||||
const tokens = Object.entries(conf.metadata.plugins[plugin].inputs).filter(([key, value]) => (!/^plugin_/.test(key))&&(value.type === "token")).map(([key]) => key)
|
||||
for (const token of tokens) {
|
||||
if ((!settings.plugins[plugin][token])||(mock === "force")) {
|
||||
console.debug(`metrics/app > using mocked token for ${plugin}.${token}`)
|
||||
settings.plugins[plugin][token] = "MOCKED_TOKEN"
|
||||
}
|
||||
}
|
||||
}
|
||||
//Process mocking and default plugin state
|
||||
for (const plugin of Object.keys(Plugins).filter(x => !["base", "core"].includes(x))) {
|
||||
//Initialization
|
||||
const {settings} = conf
|
||||
if (!settings.plugins[plugin])
|
||||
settings.plugins[plugin] = {}
|
||||
//Auto-enable plugin if needed
|
||||
if (conf.settings["plugins.default"])
|
||||
settings.plugins[plugin].enabled = settings.plugins[plugin].enabled ?? (console.debug(`metrics/app > auto-enabling ${plugin}`), true)
|
||||
//Mock plugins tokens if they're undefined
|
||||
if (mock) {
|
||||
const tokens = Object.entries(conf.metadata.plugins[plugin].inputs).filter(([key, value]) => (!/^plugin_/.test(key)) && (value.type === "token")).map(([key]) => key)
|
||||
for (const token of tokens) {
|
||||
if ((!settings.plugins[plugin][token]) || (mock === "force")) {
|
||||
console.debug(`metrics/app > using mocked token for ${plugin}.${token}`)
|
||||
settings.plugins[plugin][token] = "MOCKED_TOKEN"
|
||||
}
|
||||
}
|
||||
if (((mock)&&(!conf.settings.token))||(mock === "force")) {
|
||||
console.debug("metrics/app > using mocked token")
|
||||
conf.settings.token = "MOCKED_TOKEN"
|
||||
}
|
||||
if (debug)
|
||||
console.debug(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256}))
|
||||
|
||||
//Load octokits
|
||||
const api = {graphql:octokit.graphql.defaults({headers:{authorization:`token ${token}`}}), rest:new OctokitRest.Octokit({auth:token})}
|
||||
//Apply mocking if needed
|
||||
if (mock)
|
||||
Object.assign(api, await mocks(api))
|
||||
const {graphql, rest} = api
|
||||
|
||||
//Setup server
|
||||
const app = express()
|
||||
app.use(compression())
|
||||
const middlewares = []
|
||||
//Rate limiter middleware
|
||||
if (ratelimiter) {
|
||||
app.set("trust proxy", 1)
|
||||
middlewares.push(ratelimit({
|
||||
skip(req, _res) {
|
||||
return !!cache.get(req.params.login)
|
||||
},
|
||||
message:"Too many requests: retry later",
|
||||
headers:true,
|
||||
...ratelimiter,
|
||||
}))
|
||||
}
|
||||
//Cache headers middleware
|
||||
middlewares.push((req, res, next) => {
|
||||
const maxage = Math.round(Number(req.query.cache))
|
||||
if ((cached)||(maxage > 0))
|
||||
res.header("Cache-Control", `public, max-age=${Math.round((maxage > 0 ? maxage : cached)/1000)}`)
|
||||
else
|
||||
res.header("Cache-Control", "no-store, no-cache")
|
||||
next()
|
||||
})
|
||||
|
||||
//Base routes
|
||||
const limiter = ratelimit({max:debug ? Number.MAX_SAFE_INTEGER : 60, windowMs:60*1000, headers:false})
|
||||
const metadata = Object.fromEntries(Object.entries(conf.metadata.plugins)
|
||||
.map(([key, value]) => [key, Object.fromEntries(Object.entries(value).filter(([key]) => ["name", "icon", "categorie", "web", "supports"].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]))
|
||||
const enabled = Object.entries(metadata).filter(([_name, {categorie}]) => categorie !== "core").map(([name]) => ({name, 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 actions = {flush:new Map()}
|
||||
let requests = {limit:0, used:0, remaining:0, reset:NaN}
|
||||
if (!conf.settings.notoken) {
|
||||
requests = (await rest.rateLimit.get()).data.rate
|
||||
setInterval(async() => {
|
||||
try {
|
||||
requests = (await rest.rateLimit.get()).data.rate
|
||||
}
|
||||
catch {
|
||||
console.debug("metrics/app > failed to update remaining requests")
|
||||
}
|
||||
}, 5*60*1000)
|
||||
}
|
||||
//Web
|
||||
app.get("/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`))
|
||||
app.get("/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`))
|
||||
app.get("/favicon.ico", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/favicon.png`))
|
||||
app.get("/.favicon.png", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/favicon.png`))
|
||||
app.get("/.opengraph.png", limiter, (req, res) => conf.settings.web?.opengraph ? res.redirect(conf.settings.web?.opengraph) : res.sendFile(`${conf.paths.statics}/opengraph.png`))
|
||||
//Plugins and templates
|
||||
app.get("/.plugins", limiter, (req, res) => res.status(200).json(enabled))
|
||||
app.get("/.plugins.base", limiter, (req, res) => res.status(200).json(conf.settings.plugins.base.parts))
|
||||
app.get("/.plugins.metadata", limiter, (req, res) => res.status(200).json(metadata))
|
||||
app.get("/.templates", limiter, (req, res) => res.status(200).json(templates))
|
||||
app.get("/.templates/:template", limiter, (req, res) => req.params.template in conf.templates ? res.status(200).json(conf.templates[req.params.template]) : res.sendStatus(404))
|
||||
for (const template in conf.templates)
|
||||
app.use(`/.templates/${template}/partials`, express.static(`${conf.paths.templates}/${template}/partials`))
|
||||
//Styles
|
||||
app.get("/.css/style.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.css`))
|
||||
app.get("/.css/style.vars.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.vars.css`))
|
||||
app.get("/.css/style.prism.css", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/themes/prism-tomorrow.css`))
|
||||
//Scripts
|
||||
app.get("/.js/app.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.js`))
|
||||
app.get("/.js/app.placeholder.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.placeholder.js`))
|
||||
app.get("/.js/ejs.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/ejs/ejs.min.js`))
|
||||
app.get("/.js/faker.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/faker/dist/faker.min.js`))
|
||||
app.get("/.js/axios.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/axios/dist/axios.min.js`))
|
||||
app.get("/.js/axios.min.map", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/axios/dist/axios.min.map`))
|
||||
app.get("/.js/vue.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue/dist/vue.min.js`))
|
||||
app.get("/.js/vue.prism.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue-prism-component/dist/vue-prism-component.min.js`))
|
||||
app.get("/.js/vue-prism-component.min.js.map", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue-prism-component/dist/vue-prism-component.min.js.map`))
|
||||
app.get("/.js/prism.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/prism.js`))
|
||||
app.get("/.js/prism.yaml.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-yaml.min.js`))
|
||||
app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-markdown.min.js`))
|
||||
//Meta
|
||||
app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version))
|
||||
app.get("/.requests", limiter, (req, res) => res.status(200).json(requests))
|
||||
app.get("/.hosted", limiter, (req, res) => res.status(200).json(conf.settings.hosted || null))
|
||||
//Cache
|
||||
app.get("/.uncache", limiter, (req, res) => {
|
||||
const {token, user} = req.query
|
||||
if (token) {
|
||||
if (actions.flush.has(token)) {
|
||||
console.debug(`metrics/app/${actions.flush.get(token)} > flushed cache`)
|
||||
cache.del(actions.flush.get(token))
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
{
|
||||
const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}`
|
||||
actions.flush.set(token, user)
|
||||
return res.json({token})
|
||||
}
|
||||
})
|
||||
|
||||
//About routes
|
||||
app.use("/about/.statics/", express.static(`${conf.paths.statics}/about`))
|
||||
app.get("/about/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
|
||||
app.get("/about/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
|
||||
app.get("/about/:login", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
|
||||
app.get("/about/query/:login/", ...middlewares, async(req, res) => {
|
||||
//Check username
|
||||
const login = req.params.login?.replace(/[\n\r]/g, "")
|
||||
if (!/^[-\w]+$/i.test(login)) {
|
||||
console.debug(`metrics/app/${login}/insights > 400 (invalid username)`)
|
||||
return res.status(400).send("Bad request: username seems invalid")
|
||||
}
|
||||
//Compute metrics
|
||||
try {
|
||||
//Read cached data if possible
|
||||
if ((!debug)&&(cached)&&(cache.get(`about.${login}`))) {
|
||||
console.debug(`metrics/app/${login}/insights > using cached results`)
|
||||
return res.send(cache.get(`about.${login}`))
|
||||
}
|
||||
//Compute metrics
|
||||
console.debug(`metrics/app/${login}/insights > compute insights`)
|
||||
const json = await metrics({
|
||||
login, q:{
|
||||
template:"classic",
|
||||
achievements:true, "achievements.threshold":"X",
|
||||
isocalendar:true, "isocalendar.duration":"full-year",
|
||||
languages:true, "languages.limit":0,
|
||||
activity:true, "activity.limit":100, "activity.days":0,
|
||||
notable:true,
|
||||
},
|
||||
}, {graphql, rest, plugins:{achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true}, activity:{enabled:true, markdown:"extended"}, notable:{enabled:true}}, conf, convert:"json"}, {Plugins, Templates})
|
||||
//Cache
|
||||
if ((!debug)&&(cached)) {
|
||||
const maxage = Math.round(Number(req.query.cache))
|
||||
cache.put(`about.${login}`, json, maxage > 0 ? maxage : cached)
|
||||
}
|
||||
return res.json(json)
|
||||
}
|
||||
//Internal error
|
||||
catch (error) {
|
||||
//Not found user
|
||||
if ((error instanceof Error)&&(/^user not found$/.test(error.message))) {
|
||||
console.debug(`metrics/app/${login} > 404 (user/organization not found)`)
|
||||
return res.status(404).send("Not found: unknown user or organization")
|
||||
}
|
||||
//GitHub failed request
|
||||
if ((error instanceof Error)&&(/this may be the result of a timeout, or it could be a GitHub bug/i.test(error.errors?.[0]?.message))) {
|
||||
console.debug(`metrics/app/${login} > 502 (bad gateway from GitHub)`)
|
||||
const request = encodeURIComponent(error.errors[0].message.match(/`(?<request>[\w:]+)`/)?.groups?.request ?? "").replace(/%3A/g, ":")
|
||||
return res.status(500).send(`Internal Server Error: failed to execute request ${request} (this may be the result of a timeout, or it could be a GitHub bug)`)
|
||||
}
|
||||
//General error
|
||||
console.error(error)
|
||||
return res.status(500).send("Internal Server Error: failed to process metrics correctly")
|
||||
}
|
||||
})
|
||||
|
||||
//Metrics
|
||||
const pending = new Map()
|
||||
app.get("/:login/:repository?", ...middlewares, async(req, res) => {
|
||||
//Request params
|
||||
const login = req.params.login?.replace(/[\n\r]/g, "")
|
||||
const repository = req.params.repository?.replace(/[\n\r]/g, "")
|
||||
let solve = null
|
||||
//Check username
|
||||
if (!/^[-\w]+$/i.test(login)) {
|
||||
console.debug(`metrics/app/${login} > 400 (invalid username)`)
|
||||
return res.status(400).send("Bad request: username seems invalid")
|
||||
}
|
||||
//Allowed list check
|
||||
if ((restricted.length)&&(!restricted.includes(login))) {
|
||||
console.debug(`metrics/app/${login} > 403 (not in allowed users)`)
|
||||
return res.status(403).send("Forbidden: username not in allowed list")
|
||||
}
|
||||
//Prevent multiples requests
|
||||
if ((!debug)&&(!mock)&&(pending.has(login))) {
|
||||
console.debug(`metrics/app/${login} > awaiting pending request`)
|
||||
await pending.get(login)
|
||||
}
|
||||
else
|
||||
pending.set(login, new Promise(_solve => solve = _solve))
|
||||
//Read cached data if possible
|
||||
if ((!debug)&&(cached)&&(cache.get(login))) {
|
||||
console.debug(`metrics/app/${login} > using cached image`)
|
||||
const {rendered, mime} = cache.get(login)
|
||||
res.header("Content-Type", mime)
|
||||
return res.send(rendered)
|
||||
}
|
||||
//Maximum simultaneous users
|
||||
if ((maxusers)&&(cache.size()+1 > maxusers)) {
|
||||
console.debug(`metrics/app/${login} > 503 (maximum users reached)`)
|
||||
return res.status(503).send("Service Unavailable: maximum number of users reached, only cached metrics are available")
|
||||
}
|
||||
//Repository alias
|
||||
if (repository) {
|
||||
console.debug(`metrics/app/${login} > compute repository metrics`)
|
||||
if (!req.query.template)
|
||||
req.query.template = "repository"
|
||||
req.query.repo = repository
|
||||
}
|
||||
|
||||
//Compute rendering
|
||||
try {
|
||||
//Render
|
||||
const q = req.query
|
||||
console.debug(`metrics/app/${login} > ${util.inspect(q, {depth:Infinity, maxStringLength:256})}`)
|
||||
const {rendered, mime} = await metrics({login, q}, {
|
||||
graphql, rest, plugins, conf,
|
||||
die:q["plugins.errors.fatal"] ?? false,
|
||||
verify:q.verify ?? false,
|
||||
convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(q["config.output"]) ? q["config.output"] : null,
|
||||
}, {Plugins, Templates})
|
||||
//Cache
|
||||
if ((!debug)&&(cached)) {
|
||||
const maxage = Math.round(Number(req.query.cache))
|
||||
cache.put(login, {rendered, mime}, maxage > 0 ? maxage : cached)
|
||||
}
|
||||
//Send response
|
||||
res.header("Content-Type", mime)
|
||||
return res.send(rendered)
|
||||
}
|
||||
//Internal error
|
||||
catch (error) {
|
||||
//Not found user
|
||||
if ((error instanceof Error)&&(/^user not found$/.test(error.message))) {
|
||||
console.debug(`metrics/app/${login} > 404 (user/organization not found)`)
|
||||
return res.status(404).send("Not found: unknown user or organization")
|
||||
}
|
||||
//Invalid template
|
||||
if ((error instanceof Error)&&(/^unsupported template$/.test(error.message))) {
|
||||
console.debug(`metrics/app/${login} > 400 (bad request)`)
|
||||
return res.status(400).send("Bad request: unsupported template")
|
||||
}
|
||||
//Unsupported output format or account type
|
||||
if ((error instanceof Error)&&(/^not supported for: [\s\S]*$/.test(error.message))) {
|
||||
console.debug(`metrics/app/${login} > 406 (Not Acceptable)`)
|
||||
return res.status(406).send("Not Acceptable: unsupported output format or account type for specified parameters")
|
||||
}
|
||||
//GitHub failed request
|
||||
if ((error instanceof Error)&&(/this may be the result of a timeout, or it could be a GitHub bug/i.test(error.errors?.[0]?.message))) {
|
||||
console.debug(`metrics/app/${login} > 502 (bad gateway from GitHub)`)
|
||||
const request = encodeURIComponent(error.errors[0].message.match(/`(?<request>[\w:]+)`/)?.groups?.request ?? "").replace(/%3A/g, ":")
|
||||
return res.status(500).send(`Internal Server Error: failed to execute request ${request} (this may be the result of a timeout, or it could be a GitHub bug)`)
|
||||
}
|
||||
//General error
|
||||
console.error(error)
|
||||
return res.status(500).send("Internal Server Error: failed to process metrics correctly")
|
||||
}
|
||||
//After rendering
|
||||
finally {
|
||||
solve?.()
|
||||
}
|
||||
})
|
||||
|
||||
//Listen
|
||||
app.listen(port, () => console.log([
|
||||
`Listening on port │ ${port}`,
|
||||
`Debug mode │ ${debug}`,
|
||||
`Mocked data │ ${conf.settings.mocked ?? false}`,
|
||||
`Restricted to users │ ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
|
||||
`Cached time │ ${cached} seconds`,
|
||||
`Rate limiter │ ${ratelimiter ? util.inspect(ratelimiter, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`,
|
||||
`Max simultaneous users │ ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
|
||||
`Plugins enabled │ ${enabled.map(({name}) => name).join(", ")}`,
|
||||
`SVG optimization │ ${conf.settings.optimize ?? false}`,
|
||||
"Server ready !",
|
||||
].join("\n")))
|
||||
}
|
||||
}
|
||||
if (((mock) && (!conf.settings.token)) || (mock === "force")) {
|
||||
console.debug("metrics/app > using mocked token")
|
||||
conf.settings.token = "MOCKED_TOKEN"
|
||||
}
|
||||
if (debug)
|
||||
console.debug(util.inspect(conf.settings, {depth:Infinity, maxStringLength:256}))
|
||||
|
||||
//Load octokits
|
||||
const api = {graphql:octokit.graphql.defaults({headers:{authorization:`token ${token}`}}), rest:new OctokitRest.Octokit({auth:token})}
|
||||
//Apply mocking if needed
|
||||
if (mock)
|
||||
Object.assign(api, await mocks(api))
|
||||
const {graphql, rest} = api
|
||||
|
||||
//Setup server
|
||||
const app = express()
|
||||
app.use(compression())
|
||||
const middlewares = []
|
||||
//Rate limiter middleware
|
||||
if (ratelimiter) {
|
||||
app.set("trust proxy", 1)
|
||||
middlewares.push(ratelimit({
|
||||
skip(req, _res) {
|
||||
return !!cache.get(req.params.login)
|
||||
},
|
||||
message:"Too many requests: retry later",
|
||||
headers:true,
|
||||
...ratelimiter,
|
||||
}))
|
||||
}
|
||||
//Cache headers middleware
|
||||
middlewares.push((req, res, next) => {
|
||||
const maxage = Math.round(Number(req.query.cache))
|
||||
if ((cached) || (maxage > 0))
|
||||
res.header("Cache-Control", `public, max-age=${Math.round((maxage > 0 ? maxage : cached) / 1000)}`)
|
||||
else
|
||||
res.header("Cache-Control", "no-store, no-cache")
|
||||
next()
|
||||
})
|
||||
|
||||
//Base routes
|
||||
const limiter = ratelimit({max:debug ? Number.MAX_SAFE_INTEGER : 60, windowMs:60 * 1000, headers:false})
|
||||
const metadata = Object.fromEntries(
|
||||
Object.entries(conf.metadata.plugins)
|
||||
.map(([key, value]) => [key, Object.fromEntries(Object.entries(value).filter(([key]) => ["name", "icon", "categorie", "web", "supports"].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]),
|
||||
)
|
||||
const enabled = Object.entries(metadata).filter(([_name, {categorie}]) => categorie !== "core").map(([name]) => ({name, 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 actions = {flush:new Map()}
|
||||
let requests = {limit:0, used:0, remaining:0, reset:NaN}
|
||||
if (!conf.settings.notoken) {
|
||||
requests = (await rest.rateLimit.get()).data.rate
|
||||
setInterval(async () => {
|
||||
try {
|
||||
requests = (await rest.rateLimit.get()).data.rate
|
||||
}
|
||||
catch {
|
||||
console.debug("metrics/app > failed to update remaining requests")
|
||||
}
|
||||
}, 5 * 60 * 1000)
|
||||
}
|
||||
//Web
|
||||
app.get("/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`))
|
||||
app.get("/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/index.html`))
|
||||
app.get("/favicon.ico", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/favicon.png`))
|
||||
app.get("/.favicon.png", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/favicon.png`))
|
||||
app.get("/.opengraph.png", limiter, (req, res) => conf.settings.web?.opengraph ? res.redirect(conf.settings.web?.opengraph) : res.sendFile(`${conf.paths.statics}/opengraph.png`))
|
||||
//Plugins and templates
|
||||
app.get("/.plugins", limiter, (req, res) => res.status(200).json(enabled))
|
||||
app.get("/.plugins.base", limiter, (req, res) => res.status(200).json(conf.settings.plugins.base.parts))
|
||||
app.get("/.plugins.metadata", limiter, (req, res) => res.status(200).json(metadata))
|
||||
app.get("/.templates", limiter, (req, res) => res.status(200).json(templates))
|
||||
app.get("/.templates/:template", limiter, (req, res) => req.params.template in conf.templates ? res.status(200).json(conf.templates[req.params.template]) : res.sendStatus(404))
|
||||
for (const template in conf.templates)
|
||||
app.use(`/.templates/${template}/partials`, express.static(`${conf.paths.templates}/${template}/partials`))
|
||||
//Styles
|
||||
app.get("/.css/style.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.css`))
|
||||
app.get("/.css/style.vars.css", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/style.vars.css`))
|
||||
app.get("/.css/style.prism.css", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/themes/prism-tomorrow.css`))
|
||||
//Scripts
|
||||
app.get("/.js/app.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.js`))
|
||||
app.get("/.js/app.placeholder.js", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/app.placeholder.js`))
|
||||
app.get("/.js/ejs.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/ejs/ejs.min.js`))
|
||||
app.get("/.js/faker.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/faker/dist/faker.min.js`))
|
||||
app.get("/.js/axios.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/axios/dist/axios.min.js`))
|
||||
app.get("/.js/axios.min.map", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/axios/dist/axios.min.map`))
|
||||
app.get("/.js/vue.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue/dist/vue.min.js`))
|
||||
app.get("/.js/vue.prism.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue-prism-component/dist/vue-prism-component.min.js`))
|
||||
app.get("/.js/vue-prism-component.min.js.map", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/vue-prism-component/dist/vue-prism-component.min.js.map`))
|
||||
app.get("/.js/prism.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/prism.js`))
|
||||
app.get("/.js/prism.yaml.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-yaml.min.js`))
|
||||
app.get("/.js/prism.markdown.min.js", limiter, (req, res) => res.sendFile(`${conf.paths.node_modules}/prismjs/components/prism-markdown.min.js`))
|
||||
//Meta
|
||||
app.get("/.version", limiter, (req, res) => res.status(200).send(conf.package.version))
|
||||
app.get("/.requests", limiter, (req, res) => res.status(200).json(requests))
|
||||
app.get("/.hosted", limiter, (req, res) => res.status(200).json(conf.settings.hosted || null))
|
||||
//Cache
|
||||
app.get("/.uncache", limiter, (req, res) => {
|
||||
const {token, user} = req.query
|
||||
if (token) {
|
||||
if (actions.flush.has(token)) {
|
||||
console.debug(`metrics/app/${actions.flush.get(token)} > flushed cache`)
|
||||
cache.del(actions.flush.get(token))
|
||||
return res.sendStatus(200)
|
||||
}
|
||||
return res.sendStatus(400)
|
||||
}
|
||||
{
|
||||
const token = `${Math.random().toString(16).replace("0.", "")}${Math.random().toString(16).replace("0.", "")}`
|
||||
actions.flush.set(token, user)
|
||||
return res.json({token})
|
||||
}
|
||||
})
|
||||
|
||||
//About routes
|
||||
app.use("/about/.statics/", express.static(`${conf.paths.statics}/about`))
|
||||
app.get("/about/", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
|
||||
app.get("/about/index.html", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
|
||||
app.get("/about/:login", limiter, (req, res) => res.sendFile(`${conf.paths.statics}/about/index.html`))
|
||||
app.get("/about/query/:login/", ...middlewares, async (req, res) => {
|
||||
//Check username
|
||||
const login = req.params.login?.replace(/[\n\r]/g, "")
|
||||
if (!/^[-\w]+$/i.test(login)) {
|
||||
console.debug(`metrics/app/${login}/insights > 400 (invalid username)`)
|
||||
return res.status(400).send("Bad request: username seems invalid")
|
||||
}
|
||||
//Compute metrics
|
||||
try {
|
||||
//Read cached data if possible
|
||||
if ((!debug) && (cached) && (cache.get(`about.${login}`))) {
|
||||
console.debug(`metrics/app/${login}/insights > using cached results`)
|
||||
return res.send(cache.get(`about.${login}`))
|
||||
}
|
||||
//Compute metrics
|
||||
console.debug(`metrics/app/${login}/insights > compute insights`)
|
||||
const json = await metrics(
|
||||
{
|
||||
login,
|
||||
q:{
|
||||
template:"classic",
|
||||
achievements:true,
|
||||
"achievements.threshold":"X",
|
||||
isocalendar:true,
|
||||
"isocalendar.duration":"full-year",
|
||||
languages:true,
|
||||
"languages.limit":0,
|
||||
activity:true,
|
||||
"activity.limit":100,
|
||||
"activity.days":0,
|
||||
notable:true,
|
||||
},
|
||||
},
|
||||
{graphql, rest, plugins:{achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true}, activity:{enabled:true, markdown:"extended"}, notable:{enabled:true}}, conf, convert:"json"},
|
||||
{Plugins, Templates},
|
||||
)
|
||||
//Cache
|
||||
if ((!debug) && (cached)) {
|
||||
const maxage = Math.round(Number(req.query.cache))
|
||||
cache.put(`about.${login}`, json, maxage > 0 ? maxage : cached)
|
||||
}
|
||||
return res.json(json)
|
||||
}
|
||||
//Internal error
|
||||
catch (error) {
|
||||
//Not found user
|
||||
if ((error instanceof Error) && (/^user not found$/.test(error.message))) {
|
||||
console.debug(`metrics/app/${login} > 404 (user/organization not found)`)
|
||||
return res.status(404).send("Not found: unknown user or organization")
|
||||
}
|
||||
//GitHub failed request
|
||||
if ((error instanceof Error) && (/this may be the result of a timeout, or it could be a GitHub bug/i.test(error.errors?.[0]?.message))) {
|
||||
console.debug(`metrics/app/${login} > 502 (bad gateway from GitHub)`)
|
||||
const request = encodeURIComponent(error.errors[0].message.match(/`(?<request>[\w:]+)`/)?.groups?.request ?? "").replace(/%3A/g, ":")
|
||||
return res.status(500).send(`Internal Server Error: failed to execute request ${request} (this may be the result of a timeout, or it could be a GitHub bug)`)
|
||||
}
|
||||
//General error
|
||||
console.error(error)
|
||||
return res.status(500).send("Internal Server Error: failed to process metrics correctly")
|
||||
}
|
||||
})
|
||||
|
||||
//Metrics
|
||||
const pending = new Map()
|
||||
app.get("/:login/:repository?", ...middlewares, async (req, res) => {
|
||||
//Request params
|
||||
const login = req.params.login?.replace(/[\n\r]/g, "")
|
||||
const repository = req.params.repository?.replace(/[\n\r]/g, "")
|
||||
let solve = null
|
||||
//Check username
|
||||
if (!/^[-\w]+$/i.test(login)) {
|
||||
console.debug(`metrics/app/${login} > 400 (invalid username)`)
|
||||
return res.status(400).send("Bad request: username seems invalid")
|
||||
}
|
||||
//Allowed list check
|
||||
if ((restricted.length) && (!restricted.includes(login))) {
|
||||
console.debug(`metrics/app/${login} > 403 (not in allowed users)`)
|
||||
return res.status(403).send("Forbidden: username not in allowed list")
|
||||
}
|
||||
//Prevent multiples requests
|
||||
if ((!debug) && (!mock) && (pending.has(login))) {
|
||||
console.debug(`metrics/app/${login} > awaiting pending request`)
|
||||
await pending.get(login)
|
||||
}
|
||||
else
|
||||
pending.set(login, new Promise(_solve => solve = _solve))
|
||||
|
||||
|
||||
//Read cached data if possible
|
||||
if ((!debug) && (cached) && (cache.get(login))) {
|
||||
console.debug(`metrics/app/${login} > using cached image`)
|
||||
const {rendered, mime} = cache.get(login)
|
||||
res.header("Content-Type", mime)
|
||||
return res.send(rendered)
|
||||
}
|
||||
//Maximum simultaneous users
|
||||
if ((maxusers) && (cache.size() + 1 > maxusers)) {
|
||||
console.debug(`metrics/app/${login} > 503 (maximum users reached)`)
|
||||
return res.status(503).send("Service Unavailable: maximum number of users reached, only cached metrics are available")
|
||||
}
|
||||
//Repository alias
|
||||
if (repository) {
|
||||
console.debug(`metrics/app/${login} > compute repository metrics`)
|
||||
if (!req.query.template)
|
||||
req.query.template = "repository"
|
||||
req.query.repo = repository
|
||||
}
|
||||
|
||||
//Compute rendering
|
||||
try {
|
||||
//Render
|
||||
const q = req.query
|
||||
console.debug(`metrics/app/${login} > ${util.inspect(q, {depth:Infinity, maxStringLength:256})}`)
|
||||
const {rendered, mime} = await metrics({login, q}, {
|
||||
graphql,
|
||||
rest,
|
||||
plugins,
|
||||
conf,
|
||||
die:q["plugins.errors.fatal"] ?? false,
|
||||
verify:q.verify ?? false,
|
||||
convert:["svg", "jpeg", "png", "json", "markdown", "markdown-pdf"].includes(q["config.output"]) ? q["config.output"] : null,
|
||||
}, {Plugins, Templates})
|
||||
//Cache
|
||||
if ((!debug) && (cached)) {
|
||||
const maxage = Math.round(Number(req.query.cache))
|
||||
cache.put(login, {rendered, mime}, maxage > 0 ? maxage : cached)
|
||||
}
|
||||
//Send response
|
||||
res.header("Content-Type", mime)
|
||||
return res.send(rendered)
|
||||
}
|
||||
//Internal error
|
||||
catch (error) {
|
||||
//Not found user
|
||||
if ((error instanceof Error) && (/^user not found$/.test(error.message))) {
|
||||
console.debug(`metrics/app/${login} > 404 (user/organization not found)`)
|
||||
return res.status(404).send("Not found: unknown user or organization")
|
||||
}
|
||||
//Invalid template
|
||||
if ((error instanceof Error) && (/^unsupported template$/.test(error.message))) {
|
||||
console.debug(`metrics/app/${login} > 400 (bad request)`)
|
||||
return res.status(400).send("Bad request: unsupported template")
|
||||
}
|
||||
//Unsupported output format or account type
|
||||
if ((error instanceof Error) && (/^not supported for: [\s\S]*$/.test(error.message))) {
|
||||
console.debug(`metrics/app/${login} > 406 (Not Acceptable)`)
|
||||
return res.status(406).send("Not Acceptable: unsupported output format or account type for specified parameters")
|
||||
}
|
||||
//GitHub failed request
|
||||
if ((error instanceof Error) && (/this may be the result of a timeout, or it could be a GitHub bug/i.test(error.errors?.[0]?.message))) {
|
||||
console.debug(`metrics/app/${login} > 502 (bad gateway from GitHub)`)
|
||||
const request = encodeURIComponent(error.errors[0].message.match(/`(?<request>[\w:]+)`/)?.groups?.request ?? "").replace(/%3A/g, ":")
|
||||
return res.status(500).send(`Internal Server Error: failed to execute request ${request} (this may be the result of a timeout, or it could be a GitHub bug)`)
|
||||
}
|
||||
//General error
|
||||
console.error(error)
|
||||
return res.status(500).send("Internal Server Error: failed to process metrics correctly")
|
||||
}
|
||||
finally {
|
||||
//After rendering
|
||||
|
||||
solve?.()
|
||||
}
|
||||
})
|
||||
|
||||
//Listen
|
||||
app.listen(port, () => console.log([
|
||||
`Listening on port │ ${port}`,
|
||||
`Debug mode │ ${debug}`,
|
||||
`Mocked data │ ${conf.settings.mocked ?? false}`,
|
||||
`Restricted to users │ ${restricted.size ? [...restricted].join(", ") : "(unrestricted)"}`,
|
||||
`Cached time │ ${cached} seconds`,
|
||||
`Rate limiter │ ${ratelimiter ? util.inspect(ratelimiter, {depth:Infinity, maxStringLength:256}) : "(enabled)"}`,
|
||||
`Max simultaneous users │ ${maxusers ? `${maxusers} users` : "(unrestricted)"}`,
|
||||
`Plugins enabled │ ${enabled.map(({name}) => name).join(", ")}`,
|
||||
`SVG optimization │ ${conf.settings.optimize ?? false}`,
|
||||
"Server ready !",
|
||||
].join("\n")))
|
||||
}
|
||||
|
||||
@@ -1,143 +1,145 @@
|
||||
;(async function() {
|
||||
//App
|
||||
return new Vue({
|
||||
//Initialization
|
||||
el:"main",
|
||||
async mounted() {
|
||||
//Palette
|
||||
try {
|
||||
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
||||
} catch (error) {}
|
||||
//User
|
||||
const user = location.pathname.split("/").pop()
|
||||
if ((user)&&(user !== "about")) {
|
||||
this.user = user
|
||||
await this.search()
|
||||
}
|
||||
else
|
||||
this.searchable = true
|
||||
//Embed
|
||||
this.embed = !!(new URLSearchParams(location.search).get("embed"))
|
||||
//Init
|
||||
await Promise.all([
|
||||
//GitHub limit tracker
|
||||
(async () => {
|
||||
const {data:requests} = await axios.get("/.requests")
|
||||
this.requests = requests
|
||||
})(),
|
||||
//Version
|
||||
(async () => {
|
||||
const {data:version} = await axios.get("/.version")
|
||||
this.version = `v${version}`
|
||||
})(),
|
||||
//Hosted
|
||||
(async () => {
|
||||
const {data:hosted} = await axios.get("/.hosted")
|
||||
this.hosted = hosted
|
||||
})(),
|
||||
])
|
||||
return new Vue({
|
||||
//Initialization
|
||||
el: "main",
|
||||
async mounted() {
|
||||
//Palette
|
||||
try {
|
||||
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
||||
}
|
||||
catch (error) {}
|
||||
//User
|
||||
const user = location.pathname.split("/").pop()
|
||||
if ((user) && (user !== "about")) {
|
||||
this.user = user
|
||||
await this.search()
|
||||
}
|
||||
else {
|
||||
this.searchable = true
|
||||
}
|
||||
//Embed
|
||||
this.embed = !!(new URLSearchParams(location.search).get("embed"))
|
||||
//Init
|
||||
await Promise.all([
|
||||
//GitHub limit tracker
|
||||
(async () => {
|
||||
const { data: requests } = await axios.get("/.requests")
|
||||
this.requests = requests
|
||||
})(),
|
||||
//Version
|
||||
(async () => {
|
||||
const { data: version } = await axios.get("/.version")
|
||||
this.version = `v${version}`
|
||||
})(),
|
||||
//Hosted
|
||||
(async () => {
|
||||
const { data: hosted } = await axios.get("/.hosted")
|
||||
this.hosted = hosted
|
||||
})(),
|
||||
])
|
||||
},
|
||||
//Watchers
|
||||
watch: {
|
||||
palette: {
|
||||
immediate: true,
|
||||
handler(current, previous) {
|
||||
document.querySelector("body").classList.remove(previous)
|
||||
document.querySelector("body").classList.add(current)
|
||||
},
|
||||
//Watchers
|
||||
watch:{
|
||||
palette:{
|
||||
immediate:true,
|
||||
handler(current, previous) {
|
||||
document.querySelector("body").classList.remove(previous)
|
||||
document.querySelector("body").classList.add(current)
|
||||
}
|
||||
}
|
||||
},
|
||||
//Methods
|
||||
methods:{
|
||||
format(type, value, options) {
|
||||
switch (type) {
|
||||
case "number":
|
||||
return new Intl.NumberFormat(navigator.lang, options).format(value)
|
||||
case "date":
|
||||
return new Intl.DateTimeFormat(navigator.lang, options).format(new Date(value))
|
||||
case "comment":
|
||||
const baseUrl = String.raw`https?:\/\/(?:www\.)?github.com\/([\w.-]+\/[\w.-]+)\/`
|
||||
return value
|
||||
.replace(
|
||||
RegExp(baseUrl + String.raw`(?:issues|pull|discussions)\/(\d+)(?:\?\S+)?(#\S+)?`, 'g'),
|
||||
(_, repo, id, comment) => (options?.repo === repo ? '' : repo) + `#${id}` + (comment ? ` (comment)` : '')
|
||||
) // -> 'lowlighter/metrics#123'
|
||||
.replace(
|
||||
RegExp(baseUrl + String.raw`commit\/([\da-f]+)`, 'g'),
|
||||
(_, repo, sha) => (options?.repo === repo ? '' : repo + '@') + sha
|
||||
) // -> 'lowlighter/metrics@123abc'
|
||||
.replace(
|
||||
RegExp(baseUrl + String.raw`compare\/(\S+...\S+)`, 'g'),
|
||||
(_, repo, tags) => (options?.repo === repo ? '' : repo + '@') + tags
|
||||
) // -> 'lowlighter/metrics@1.0...1.1'
|
||||
}
|
||||
},
|
||||
},
|
||||
//Methods
|
||||
methods: {
|
||||
format(type, value, options) {
|
||||
switch (type) {
|
||||
case "number":
|
||||
return new Intl.NumberFormat(navigator.lang, options).format(value)
|
||||
case "date":
|
||||
return new Intl.DateTimeFormat(navigator.lang, options).format(new Date(value))
|
||||
case "comment":
|
||||
const baseUrl = String.raw`https?:\/\/(?:www\.)?github.com\/([\w.-]+\/[\w.-]+)\/`
|
||||
return value
|
||||
},
|
||||
async search() {
|
||||
try {
|
||||
this.error = null
|
||||
this.metrics = null
|
||||
this.pending = true
|
||||
this.metrics = (await axios.get(`/about/query/${this.user}`)).data
|
||||
}
|
||||
catch (error) {
|
||||
this.error = {code:error.response.status, message:error.response.data}
|
||||
}
|
||||
finally {
|
||||
this.pending = false
|
||||
}
|
||||
}
|
||||
},
|
||||
//Computed properties
|
||||
computed:{
|
||||
ranked() {
|
||||
return this.metrics?.rendered.plugins.achievements.list?.filter(({leaderboard}) => leaderboard).sort((a, b) => a.leaderboard.type.localeCompare(b.leaderboard.type)) ?? []
|
||||
},
|
||||
achievements() {
|
||||
return this.metrics?.rendered.plugins.achievements.list?.filter(({leaderboard}) => !leaderboard).filter(({title}) => !/(?:automater|octonaut|infographile)/i.test(title)) ?? []
|
||||
},
|
||||
isocalendar() {
|
||||
return (this.metrics?.rendered.plugins.isocalendar.svg ?? "")
|
||||
.replace(/#ebedf0/gi, "var(--color-calendar-graph-day-bg)")
|
||||
.replace(/#9be9a8/gi, "var(--color-calendar-graph-day-L1-bg)")
|
||||
.replace(/#40c463/gi, "var(--color-calendar-graph-day-L2-bg)")
|
||||
.replace(/#30a14e/gi, "var(--color-calendar-graph-day-L3-bg)")
|
||||
.replace(/#216e39/gi, "var(--color-calendar-graph-day-L4-bg)")
|
||||
},
|
||||
languages() {
|
||||
return this.metrics?.rendered.plugins.languages.favorites ?? []
|
||||
},
|
||||
activity() {
|
||||
return this.metrics?.rendered.plugins.activity.events ?? []
|
||||
},
|
||||
contributions() {
|
||||
return this.metrics?.rendered.plugins.notable.contributions ?? []
|
||||
},
|
||||
account() {
|
||||
if (!this.metrics)
|
||||
return null
|
||||
const {login, name} = this.metrics.rendered.user
|
||||
return {login, name, avatar:this.metrics.rendered.computed.avatar, type:this.metrics?.rendered.account}
|
||||
},
|
||||
url() {
|
||||
return `${window.location.protocol}//${window.location.host}/about/${this.user}`
|
||||
},
|
||||
preview() {
|
||||
return /-preview$/.test(this.version)
|
||||
}
|
||||
},
|
||||
//Data initialization
|
||||
data:{
|
||||
version:"",
|
||||
hosted:null,
|
||||
user:"",
|
||||
embed:false,
|
||||
searchable:false,
|
||||
requests:{limit:0, used:0, remaining:0, reset:0},
|
||||
palette:"light",
|
||||
metrics:null,
|
||||
pending:false,
|
||||
error:null,
|
||||
.replace(
|
||||
RegExp(baseUrl + String.raw`(?:issues|pull|discussions)\/(\d+)(?:\?\S+)?(#\S+)?`, "g"),
|
||||
(_, repo, id, comment) => (options?.repo === repo ? "" : repo) + `#${id}` + (comment ? ` (comment)` : ""),
|
||||
) // -> 'lowlighter/metrics#123'
|
||||
.replace(
|
||||
RegExp(baseUrl + String.raw`commit\/([\da-f]+)`, "g"),
|
||||
(_, repo, sha) => (options?.repo === repo ? "" : repo + "@") + sha,
|
||||
) // -> 'lowlighter/metrics@123abc'
|
||||
.replace(
|
||||
RegExp(baseUrl + String.raw`compare\/(\S+...\S+)`, "g"),
|
||||
(_, repo, tags) => (options?.repo === repo ? "" : repo + "@") + tags,
|
||||
) // -> 'lowlighter/metrics@1.0...1.1'
|
||||
}
|
||||
})
|
||||
return value
|
||||
},
|
||||
async search() {
|
||||
try {
|
||||
this.error = null
|
||||
this.metrics = null
|
||||
this.pending = true
|
||||
this.metrics = (await axios.get(`/about/query/${this.user}`)).data
|
||||
}
|
||||
catch (error) {
|
||||
this.error = { code: error.response.status, message: error.response.data }
|
||||
}
|
||||
finally {
|
||||
this.pending = false
|
||||
}
|
||||
},
|
||||
},
|
||||
//Computed properties
|
||||
computed: {
|
||||
ranked() {
|
||||
return this.metrics?.rendered.plugins.achievements.list?.filter(({ leaderboard }) => leaderboard).sort((a, b) => a.leaderboard.type.localeCompare(b.leaderboard.type)) ?? []
|
||||
},
|
||||
achievements() {
|
||||
return this.metrics?.rendered.plugins.achievements.list?.filter(({ leaderboard }) => !leaderboard).filter(({ title }) => !/(?:automater|octonaut|infographile)/i.test(title)) ?? []
|
||||
},
|
||||
isocalendar() {
|
||||
return (this.metrics?.rendered.plugins.isocalendar.svg ?? "")
|
||||
.replace(/#ebedf0/gi, "var(--color-calendar-graph-day-bg)")
|
||||
.replace(/#9be9a8/gi, "var(--color-calendar-graph-day-L1-bg)")
|
||||
.replace(/#40c463/gi, "var(--color-calendar-graph-day-L2-bg)")
|
||||
.replace(/#30a14e/gi, "var(--color-calendar-graph-day-L3-bg)")
|
||||
.replace(/#216e39/gi, "var(--color-calendar-graph-day-L4-bg)")
|
||||
},
|
||||
languages() {
|
||||
return this.metrics?.rendered.plugins.languages.favorites ?? []
|
||||
},
|
||||
activity() {
|
||||
return this.metrics?.rendered.plugins.activity.events ?? []
|
||||
},
|
||||
contributions() {
|
||||
return this.metrics?.rendered.plugins.notable.contributions ?? []
|
||||
},
|
||||
account() {
|
||||
if (!this.metrics)
|
||||
return null
|
||||
const { login, name } = this.metrics.rendered.user
|
||||
return { login, name, avatar: this.metrics.rendered.computed.avatar, type: this.metrics?.rendered.account }
|
||||
},
|
||||
url() {
|
||||
return `${window.location.protocol}//${window.location.host}/about/${this.user}`
|
||||
},
|
||||
preview() {
|
||||
return /-preview$/.test(this.version)
|
||||
},
|
||||
},
|
||||
//Data initialization
|
||||
data: {
|
||||
version: "",
|
||||
hosted: null,
|
||||
user: "",
|
||||
embed: false,
|
||||
searchable: false,
|
||||
requests: { limit: 0, used: 0, remaining: 0, reset: 0 },
|
||||
palette: "light",
|
||||
metrics: null,
|
||||
pending: false,
|
||||
error: null,
|
||||
},
|
||||
})
|
||||
})()
|
||||
|
||||
@@ -1,256 +1,264 @@
|
||||
;(async function() {
|
||||
//Init
|
||||
const {data:metadata} = await axios.get("/.plugins.metadata")
|
||||
delete metadata.core.web.output
|
||||
delete metadata.core.web.twemojis
|
||||
const { data: metadata } = await axios.get("/.plugins.metadata")
|
||||
delete metadata.core.web.output
|
||||
delete metadata.core.web.twemojis
|
||||
//App
|
||||
return new Vue({
|
||||
//Initialization
|
||||
el:"main",
|
||||
async mounted() {
|
||||
//Interpolate config from browser
|
||||
try {
|
||||
this.config.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
||||
} catch (error) {}
|
||||
//Init
|
||||
await Promise.all([
|
||||
//GitHub limit tracker
|
||||
(async () => {
|
||||
const {data:requests} = await axios.get("/.requests")
|
||||
this.requests = requests
|
||||
})(),
|
||||
//Templates
|
||||
(async () => {
|
||||
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))
|
||||
this.templates.list = templates
|
||||
this.templates.selected = templates[0]?.name||"classic"
|
||||
})(),
|
||||
//Plugins
|
||||
(async () => {
|
||||
const {data:plugins} = await axios.get("/.plugins")
|
||||
this.plugins.list = plugins
|
||||
})(),
|
||||
//Base
|
||||
(async () => {
|
||||
const {data:base} = await axios.get("/.plugins.base")
|
||||
this.plugins.base = base
|
||||
this.plugins.enabled.base = Object.fromEntries(base.map(key => [key, true]))
|
||||
})(),
|
||||
//Version
|
||||
(async () => {
|
||||
const {data:version} = await axios.get("/.version")
|
||||
this.version = `v${version}`
|
||||
})(),
|
||||
//Hosted
|
||||
(async () => {
|
||||
const {data:hosted} = await axios.get("/.hosted")
|
||||
this.hosted = hosted
|
||||
})(),
|
||||
])
|
||||
//Generate placeholder
|
||||
this.mock({timeout:200})
|
||||
setInterval(() => {
|
||||
const marker = document.querySelector("#metrics-end")
|
||||
if (marker) {
|
||||
this.mockresize()
|
||||
marker.remove()
|
||||
}
|
||||
}, 100)
|
||||
return new Vue({
|
||||
//Initialization
|
||||
el: "main",
|
||||
async mounted() {
|
||||
//Interpolate config from browser
|
||||
try {
|
||||
this.config.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
this.palette = (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
|
||||
}
|
||||
catch (error) {}
|
||||
//Init
|
||||
await Promise.all([
|
||||
//GitHub limit tracker
|
||||
(async () => {
|
||||
const { data: requests } = await axios.get("/.requests")
|
||||
this.requests = requests
|
||||
})(),
|
||||
//Templates
|
||||
(async () => {
|
||||
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))
|
||||
this.templates.list = templates
|
||||
this.templates.selected = templates[0]?.name || "classic"
|
||||
})(),
|
||||
//Plugins
|
||||
(async () => {
|
||||
const { data: plugins } = await axios.get("/.plugins")
|
||||
this.plugins.list = plugins
|
||||
})(),
|
||||
//Base
|
||||
(async () => {
|
||||
const { data: base } = await axios.get("/.plugins.base")
|
||||
this.plugins.base = base
|
||||
this.plugins.enabled.base = Object.fromEntries(base.map(key => [key, true]))
|
||||
})(),
|
||||
//Version
|
||||
(async () => {
|
||||
const { data: version } = await axios.get("/.version")
|
||||
this.version = `v${version}`
|
||||
})(),
|
||||
//Hosted
|
||||
(async () => {
|
||||
const { data: hosted } = await axios.get("/.hosted")
|
||||
this.hosted = hosted
|
||||
})(),
|
||||
])
|
||||
//Generate placeholder
|
||||
this.mock({ timeout: 200 })
|
||||
setInterval(() => {
|
||||
const marker = document.querySelector("#metrics-end")
|
||||
if (marker) {
|
||||
this.mockresize()
|
||||
marker.remove()
|
||||
}
|
||||
}, 100)
|
||||
},
|
||||
components: { Prism: PrismComponent },
|
||||
//Watchers
|
||||
watch: {
|
||||
palette: {
|
||||
immediate: true,
|
||||
handler(current, previous) {
|
||||
document.querySelector("body").classList.remove(previous)
|
||||
document.querySelector("body").classList.add(current)
|
||||
},
|
||||
components:{Prism:PrismComponent},
|
||||
//Watchers
|
||||
watch:{
|
||||
palette:{
|
||||
immediate:true,
|
||||
handler(current, previous) {
|
||||
document.querySelector("body").classList.remove(previous)
|
||||
document.querySelector("body").classList.add(current)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
//Data initialization
|
||||
data: {
|
||||
version: "",
|
||||
user: "",
|
||||
mode: "metrics",
|
||||
tab: "overview",
|
||||
palette: "light",
|
||||
requests: { limit: 0, used: 0, remaining: 0, reset: 0 },
|
||||
cached: new Map(),
|
||||
config: Object.fromEntries(Object.entries(metadata.core.web).map(([key, { defaulted }]) => [key, defaulted])),
|
||||
metadata: Object.fromEntries(Object.entries(metadata).map(([key, { web }]) => [key, web])),
|
||||
hosted: null,
|
||||
plugins: {
|
||||
base: {},
|
||||
list: [],
|
||||
enabled: {},
|
||||
descriptions: {
|
||||
base: "🗃️ Base content",
|
||||
"base.header": "Header",
|
||||
"base.activity": "Account activity",
|
||||
"base.community": "Community stats",
|
||||
"base.repositories": "Repositories metrics",
|
||||
"base.metadata": "Metadata",
|
||||
...Object.fromEntries(Object.entries(metadata).map(([key, { name }]) => [key, name])),
|
||||
},
|
||||
//Data initialization
|
||||
data:{
|
||||
version:"",
|
||||
user:"",
|
||||
mode:"metrics",
|
||||
tab:"overview",
|
||||
palette:"light",
|
||||
requests:{limit:0, used:0, remaining:0, reset:0},
|
||||
cached:new Map(),
|
||||
config:Object.fromEntries(Object.entries(metadata.core.web).map(([key, {defaulted}]) => [key, defaulted])),
|
||||
metadata:Object.fromEntries(Object.entries(metadata).map(([key, {web}]) => [key, web])),
|
||||
hosted:null,
|
||||
plugins:{
|
||||
base:{},
|
||||
list:[],
|
||||
enabled:{},
|
||||
descriptions:{
|
||||
base:"🗃️ Base content",
|
||||
"base.header":"Header",
|
||||
"base.activity":"Account activity",
|
||||
"base.community":"Community stats",
|
||||
"base.repositories":"Repositories metrics",
|
||||
"base.metadata":"Metadata",
|
||||
...Object.fromEntries(Object.entries(metadata).map(([key, {name}]) => [key, name]))
|
||||
},
|
||||
options:{
|
||||
descriptions:{...(Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)))},
|
||||
...(Object.fromEntries(Object.entries(
|
||||
Object.assign({}, ...Object.entries(metadata).flatMap(([key, {web}]) => web)))
|
||||
.map(([key, {defaulted}]) => [key, defaulted])
|
||||
))
|
||||
},
|
||||
},
|
||||
templates:{
|
||||
list:[],
|
||||
selected:"classic",
|
||||
placeholder:{
|
||||
timeout:null,
|
||||
image:""
|
||||
},
|
||||
descriptions:{
|
||||
classic:"Classic template",
|
||||
terminal:"Terminal template",
|
||||
markdown:"(hidden)",
|
||||
repository:"(hidden)",
|
||||
},
|
||||
},
|
||||
generated:{
|
||||
pending:false,
|
||||
content:"",
|
||||
error:false,
|
||||
},
|
||||
options: {
|
||||
descriptions: { ...(Object.assign({}, ...Object.entries(metadata).flatMap(([key, { web }]) => web))) },
|
||||
...(Object.fromEntries(
|
||||
Object.entries(
|
||||
Object.assign({}, ...Object.entries(metadata).flatMap(([key, { web }]) => web)),
|
||||
)
|
||||
.map(([key, { defaulted }]) => [key, defaulted]),
|
||||
)),
|
||||
},
|
||||
//Computed data
|
||||
computed:{
|
||||
//Unusable plugins
|
||||
unusable() {
|
||||
return this.plugins.list.filter(({name}) => this.plugins.enabled[name]).filter(({enabled}) => !enabled).map(({name}) => name)
|
||||
},
|
||||
//User's avatar
|
||||
avatar() {
|
||||
return this.generated.content ? `https://github.com/${this.user}.png` : null
|
||||
},
|
||||
//User's repository
|
||||
repo() {
|
||||
return `https://github.com/${this.user}/${this.user}`
|
||||
},
|
||||
//Endpoint to use for computed metrics
|
||||
url() {
|
||||
//Plugins enabled
|
||||
const plugins = Object.entries(this.plugins.enabled)
|
||||
.flatMap(([key, value]) => key === "base" ? Object.entries(value).map(([key, value]) => [`base.${key}`, value]) : [[key, value]])
|
||||
.filter(([key, value]) => /^base[.]\w+$/.test(key) ? !value : value)
|
||||
.map(([key, value]) => `${key}=${+value}`)
|
||||
//Plugins options
|
||||
const options = Object.entries(this.plugins.options)
|
||||
.filter(([key, value]) => `${value}`.length)
|
||||
.filter(([key, value]) => this.plugins.enabled[key.split(".")[0]])
|
||||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||
//Base options
|
||||
const base = Object.entries(this.plugins.options).filter(([key, value]) => (key in metadata.base.web)&&(value !== metadata.base.web[key]?.defaulted)).map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||
//Config
|
||||
const config = Object.entries(this.config).filter(([key, value]) => (value)&&(value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => `config.${key}=${encodeURIComponent(value)}`)
|
||||
//Template
|
||||
const template = (this.templates.selected !== this.templates.list[0]) ? [`template=${this.templates.selected}`] : []
|
||||
//Generated url
|
||||
const params = [...template, ...base, ...plugins, ...options, ...config].join("&")
|
||||
return `${window.location.protocol}//${window.location.host}/${this.user}${params.length ? `?${params}` : ""}`
|
||||
},
|
||||
//Embedded generated code
|
||||
embed() {
|
||||
return ``
|
||||
},
|
||||
//GitHub action auto-generated code
|
||||
action() {
|
||||
return [
|
||||
`# Visit https://github.com/lowlighter/metrics/blob/master/action.yml for full reference`,
|
||||
`name: Metrics`,
|
||||
`on:`,
|
||||
` # Schedule updates (each hour)`,
|
||||
` schedule: [{cron: "0 * * * *"}]`,
|
||||
` # Lines below let you run workflow manually and on each commit`,
|
||||
` workflow_dispatch:`,
|
||||
` push: {branches: ["master", "main"]}`,
|
||||
`jobs:`,
|
||||
` github-metrics:`,
|
||||
` runs-on: ubuntu-latest`,
|
||||
` steps:`,
|
||||
` - uses: lowlighter/metrics@latest`,
|
||||
` with:`,
|
||||
` # Your GitHub token`,
|
||||
` token: ${"$"}{{ secrets.METRICS_TOKEN }}`,
|
||||
``,
|
||||
` # Options`,
|
||||
` user: ${this.user }`,
|
||||
` template: ${this.templates.selected}`,
|
||||
` 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]) => ` ${key.replace(/[.]/, "_")}: ${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.options).filter(([key, value]) => value).filter(([key, value]) => this.plugins.enabled[key.split(".")[0]]).map(([key, value]) => ` plugin_${key.replace(/[.]/, "_")}: ${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(/[.]/, "_")}: ${typeof value === "boolean" ? {true:"yes", false:"no"}[value] : value}`),
|
||||
].sort(),
|
||||
].join("\n")
|
||||
},
|
||||
//Configurable plugins
|
||||
configure() {
|
||||
//Check enabled plugins
|
||||
const enabled = Object.entries(this.plugins.enabled).filter(([key, value]) => (value)&&(key !== "base")).map(([key, value]) => key)
|
||||
const filter = new RegExp(`^(?:${enabled.join("|")})[.]`)
|
||||
//Search related options
|
||||
const entries = Object.entries(this.plugins.options.descriptions).filter(([key, value]) => filter.test(key))
|
||||
entries.push(...enabled.map(key => [key, this.plugins.descriptions[key]]))
|
||||
entries.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
//Return object
|
||||
const configure = Object.fromEntries(entries)
|
||||
return Object.keys(configure).length ? configure : null
|
||||
},
|
||||
//Is in preview mode
|
||||
preview() {
|
||||
return /-preview$/.test(this.version)
|
||||
}
|
||||
},
|
||||
templates: {
|
||||
list: [],
|
||||
selected: "classic",
|
||||
placeholder: {
|
||||
timeout: null,
|
||||
image: "",
|
||||
},
|
||||
//Methods
|
||||
methods:{
|
||||
//Load and render placeholder image
|
||||
async mock({timeout = 600} = {}) {
|
||||
clearTimeout(this.templates.placeholder.timeout)
|
||||
this.templates.placeholder.timeout = setTimeout(async () => {
|
||||
this.templates.placeholder.image = await placeholder(this)
|
||||
this.generated.content = ""
|
||||
this.generated.error = null
|
||||
}, timeout)
|
||||
},
|
||||
//Resize mock image
|
||||
mockresize() {
|
||||
const svg = document.querySelector(".preview .image svg")
|
||||
if ((svg)&&(svg.getAttribute("height") == 99999)) {
|
||||
const height = svg.querySelector("#metrics-end")?.getBoundingClientRect()?.y-svg.getBoundingClientRect()?.y
|
||||
if (Number.isFinite(height))
|
||||
svg.setAttribute("height", height)
|
||||
}
|
||||
},
|
||||
//Generate metrics and flush cache
|
||||
async generate() {
|
||||
//Avoid requests spamming
|
||||
if (this.generated.pending)
|
||||
return
|
||||
this.generated.pending = true
|
||||
//Compute metrics
|
||||
try {
|
||||
await axios.get(`/.uncache?&token=${(await axios.get(`/.uncache?user=${this.user}`)).data.token}`)
|
||||
this.generated.content = (await axios.get(this.url)).data
|
||||
this.generated.error = null
|
||||
} catch (error) {
|
||||
this.generated.error = {code:error.response.status, message:error.response.data}
|
||||
}
|
||||
finally {
|
||||
this.generated.pending = false
|
||||
}
|
||||
},
|
||||
descriptions: {
|
||||
classic: "Classic template",
|
||||
terminal: "Terminal template",
|
||||
markdown: "(hidden)",
|
||||
repository: "(hidden)",
|
||||
},
|
||||
})
|
||||
},
|
||||
generated: {
|
||||
pending: false,
|
||||
content: "",
|
||||
error: false,
|
||||
},
|
||||
},
|
||||
//Computed data
|
||||
computed: {
|
||||
//Unusable plugins
|
||||
unusable() {
|
||||
return this.plugins.list.filter(({ name }) => this.plugins.enabled[name]).filter(({ enabled }) => !enabled).map(({ name }) => name)
|
||||
},
|
||||
//User's avatar
|
||||
avatar() {
|
||||
return this.generated.content ? `https://github.com/${this.user}.png` : null
|
||||
},
|
||||
//User's repository
|
||||
repo() {
|
||||
return `https://github.com/${this.user}/${this.user}`
|
||||
},
|
||||
//Endpoint to use for computed metrics
|
||||
url() {
|
||||
//Plugins enabled
|
||||
const plugins = Object.entries(this.plugins.enabled)
|
||||
.flatMap(([key, value]) => key === "base" ? Object.entries(value).map(([key, value]) => [`base.${key}`, value]) : [[key, value]])
|
||||
.filter(([key, value]) => /^base[.]\w+$/.test(key) ? !value : value)
|
||||
.map(([key, value]) => `${key}=${+value}`)
|
||||
//Plugins options
|
||||
const options = Object.entries(this.plugins.options)
|
||||
.filter(([key, value]) => `${value}`.length)
|
||||
.filter(([key, value]) => this.plugins.enabled[key.split(".")[0]])
|
||||
.map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||
//Base options
|
||||
const base = Object.entries(this.plugins.options).filter(([key, value]) => (key in metadata.base.web) && (value !== metadata.base.web[key]?.defaulted)).map(([key, value]) => `${key}=${encodeURIComponent(value)}`)
|
||||
//Config
|
||||
const config = Object.entries(this.config).filter(([key, value]) => (value) && (value !== metadata.core.web[key]?.defaulted)).map(([key, value]) => `config.${key}=${encodeURIComponent(value)}`)
|
||||
//Template
|
||||
const template = (this.templates.selected !== this.templates.list[0]) ? [`template=${this.templates.selected}`] : []
|
||||
//Generated url
|
||||
const params = [...template, ...base, ...plugins, ...options, ...config].join("&")
|
||||
return `${window.location.protocol}//${window.location.host}/${this.user}${params.length ? `?${params}` : ""}`
|
||||
},
|
||||
//Embedded generated code
|
||||
embed() {
|
||||
return ``
|
||||
},
|
||||
//GitHub action auto-generated code
|
||||
action() {
|
||||
return [
|
||||
`# Visit https://github.com/lowlighter/metrics/blob/master/action.yml for full reference`,
|
||||
`name: Metrics`,
|
||||
`on:`,
|
||||
` # Schedule updates (each hour)`,
|
||||
` schedule: [{cron: "0 * * * *"}]`,
|
||||
` # Lines below let you run workflow manually and on each commit`,
|
||||
` workflow_dispatch:`,
|
||||
` push: {branches: ["master", "main"]}`,
|
||||
`jobs:`,
|
||||
` github-metrics:`,
|
||||
` runs-on: ubuntu-latest`,
|
||||
` steps:`,
|
||||
` - uses: lowlighter/metrics@latest`,
|
||||
` with:`,
|
||||
` # Your GitHub token`,
|
||||
` token: ${"$"}{{ secrets.METRICS_TOKEN }}`,
|
||||
``,
|
||||
` # Options`,
|
||||
` user: ${this.user}`,
|
||||
` template: ${this.templates.selected}`,
|
||||
` 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]) =>
|
||||
` ${key.replace(/[.]/, "_")}: ${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.options).filter(([key, value]) => value).filter(([key, value]) => this.plugins.enabled[key.split(".")[0]]).map(([key, value]) =>
|
||||
` plugin_${key.replace(/[.]/, "_")}: ${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(/[.]/, "_")}: ${typeof value === "boolean" ? { true: "yes", false: "no" }[value] : value}`),
|
||||
].sort(),
|
||||
].join("\n")
|
||||
},
|
||||
//Configurable plugins
|
||||
configure() {
|
||||
//Check enabled plugins
|
||||
const enabled = Object.entries(this.plugins.enabled).filter(([key, value]) => (value) && (key !== "base")).map(([key, value]) => key)
|
||||
const filter = new RegExp(`^(?:${enabled.join("|")})[.]`)
|
||||
//Search related options
|
||||
const entries = Object.entries(this.plugins.options.descriptions).filter(([key, value]) => filter.test(key))
|
||||
entries.push(...enabled.map(key => [key, this.plugins.descriptions[key]]))
|
||||
entries.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
//Return object
|
||||
const configure = Object.fromEntries(entries)
|
||||
return Object.keys(configure).length ? configure : null
|
||||
},
|
||||
//Is in preview mode
|
||||
preview() {
|
||||
return /-preview$/.test(this.version)
|
||||
},
|
||||
},
|
||||
//Methods
|
||||
methods: {
|
||||
//Load and render placeholder image
|
||||
async mock({ timeout = 600 } = {}) {
|
||||
clearTimeout(this.templates.placeholder.timeout)
|
||||
this.templates.placeholder.timeout = setTimeout(async () => {
|
||||
this.templates.placeholder.image = await placeholder(this)
|
||||
this.generated.content = ""
|
||||
this.generated.error = null
|
||||
}, timeout)
|
||||
},
|
||||
//Resize mock image
|
||||
mockresize() {
|
||||
const svg = document.querySelector(".preview .image svg")
|
||||
if ((svg) && (svg.getAttribute("height") == 99999)) {
|
||||
const height = svg.querySelector("#metrics-end")?.getBoundingClientRect()?.y - svg.getBoundingClientRect()?.y
|
||||
if (Number.isFinite(height))
|
||||
svg.setAttribute("height", height)
|
||||
}
|
||||
},
|
||||
//Generate metrics and flush cache
|
||||
async generate() {
|
||||
//Avoid requests spamming
|
||||
if (this.generated.pending)
|
||||
return
|
||||
this.generated.pending = true
|
||||
//Compute metrics
|
||||
try {
|
||||
await axios.get(`/.uncache?&token=${(await axios.get(`/.uncache?user=${this.user}`)).data.token}`)
|
||||
this.generated.content = (await axios.get(this.url)).data
|
||||
this.generated.error = null
|
||||
}
|
||||
catch (error) {
|
||||
this.generated.error = { code: error.response.status, message: error.response.data }
|
||||
}
|
||||
finally {
|
||||
this.generated.pending = false
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
})()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,95 +1,101 @@
|
||||
//Imports
|
||||
import * as compute from "./list/index.mjs"
|
||||
import * as compute from "./list/index.mjs"
|
||||
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, computed, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.achievements))
|
||||
return null
|
||||
export default async function({login, q, imports, data, computed, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.achievements))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {threshold, secrets, only, ignored, limit} = imports.metadata.plugins.achievements.inputs({data, q, account})
|
||||
//Load inputs
|
||||
let {threshold, secrets, only, ignored, limit} = imports.metadata.plugins.achievements.inputs({data, q, account})
|
||||
|
||||
//Initialization
|
||||
const list = []
|
||||
await total({imports})
|
||||
await compute[account]({list, login, data, computed, imports, graphql, queries, rank, leaderboard})
|
||||
//Initialization
|
||||
const list = []
|
||||
await total({imports})
|
||||
await compute[account]({list, login, data, computed, imports, graphql, queries, rank, leaderboard})
|
||||
|
||||
//Results
|
||||
const order = {S:5, A:4, B:3, C:2, $:1, X:0}
|
||||
const colors = {S:["#FF0000", "#FF8500"], A:["#B59151", "#FFD576"], B:["#7D6CFF", "#B2A8FF"], C:["#2088FF", "#79B8FF"], $:["#FF48BD", "#FF92D8"], X:["#7A7A7A", "#B0B0B0"]}
|
||||
const achievements = list
|
||||
.filter(a => (order[a.rank] >= order[threshold])||((a.rank === "$")&&(secrets)))
|
||||
.filter(a => (!only.length)||((only.length)&&(only.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))
|
||||
.map(({title, unlock, ...achievement}) => ({title:({S:`Master ${title.toLocaleLowerCase()}`, A:`Super ${title.toLocaleLowerCase()}`, B:`Great ${title.toLocaleLowerCase()}`}[achievement.rank] ?? title), unlock:!/invalid date/i.test(unlock) ? `${imports.date(unlock, {timeStyle:"short", timeZone:data.config.timezone?.name})} on ${imports.date(unlock, {dateStyle:"short", timeZone:data.config.timezone?.name})}` : null, ...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)
|
||||
return {list:achievements}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
const order = {S:5, A:4, B:3, C:2, $:1, X:0}
|
||||
const colors = {S:["#FF0000", "#FF8500"], A:["#B59151", "#FFD576"], B:["#7D6CFF", "#B2A8FF"], C:["#2088FF", "#79B8FF"], $:["#FF48BD", "#FF92D8"], X:["#7A7A7A", "#B0B0B0"]}
|
||||
const achievements = list
|
||||
.filter(a => (order[a.rank] >= order[threshold]) || ((a.rank === "$") && (secrets)))
|
||||
.filter(a => (!only.length) || ((only.length) && (only.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))
|
||||
.map(({title, unlock, ...achievement}) => ({
|
||||
title:({S:`Master ${title.toLocaleLowerCase()}`, A:`Super ${title.toLocaleLowerCase()}`, B:`Great ${title.toLocaleLowerCase()}`}[achievement.rank] ?? title),
|
||||
unlock:!/invalid date/i.test(unlock) ? `${imports.date(unlock, {timeStyle:"short", timeZone:data.config.timezone?.name})} on ${imports.date(unlock, {dateStyle:"short", timeZone:data.config.timezone?.name})}` : null,
|
||||
...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)
|
||||
return {list:achievements}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
/**Rank */
|
||||
function rank(x, [c, b, a, m]) {
|
||||
if (x >= a)
|
||||
return {rank:"A", progress:(x-a)/(m-a)}
|
||||
else if (x >= b)
|
||||
return {rank:"B", progress:(x-b)/(a-b)}
|
||||
else if (x >= c)
|
||||
return {rank:"C", progress:(x-c)/(b-c)}
|
||||
return {rank:"X", progress:x/c}
|
||||
}
|
||||
function rank(x, [c, b, a, m]) {
|
||||
if (x >= a)
|
||||
return {rank:"A", progress:(x - a) / (m - a)}
|
||||
else if (x >= b)
|
||||
return {rank:"B", progress:(x - b) / (a - b)}
|
||||
else if (x >= c)
|
||||
return {rank:"C", progress:(x - c) / (b - c)}
|
||||
return {rank:"X", progress:x / c}
|
||||
}
|
||||
|
||||
/**Leaderboards */
|
||||
function leaderboard({user, type, requirement}) {
|
||||
return requirement ? {
|
||||
user:1+user,
|
||||
function leaderboard({user, type, requirement}) {
|
||||
return requirement
|
||||
? {
|
||||
user:1 + user,
|
||||
total:total[type],
|
||||
type,
|
||||
get top() {
|
||||
return Number(`1${"0".repeat(Math.ceil(Math.log10(this.user)))}`)
|
||||
},
|
||||
get percentile() {
|
||||
return 100*(this.user/this.top)
|
||||
return 100 * (this.user / this.top)
|
||||
},
|
||||
} : null
|
||||
}
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
/**Total extracter */
|
||||
async function total({imports}) {
|
||||
if (!total.promise) {
|
||||
total.promise = new Promise(async(solve, reject) => {
|
||||
//Setup browser
|
||||
console.debug("metrics/compute/plugins > achievements > filling total from github.com/search")
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/plugins > achievements > started ${await browser.version()}`)
|
||||
//Extracting total from github.com/search
|
||||
for (let i = 0; (i < 100)&&((!total.users)||(!total.repositories)); i++) {
|
||||
const page = await browser.newPage()
|
||||
await page.goto("https://github.com/search")
|
||||
const result = await page.evaluate(() => [...document.querySelectorAll("h2")].filter(node => /Search more/.test(node.innerText)).shift()?.innerText.trim().match(/(?<count>\d+)M\s+(?<type>repositories|users|issues)$/)?.groups) ?? null
|
||||
console.log(`metrics/compute/plugins > achievements > setup found ${result?.type ?? "(?)"}`)
|
||||
if ((result?.type)&&(!total[result.type])) {
|
||||
const {count, type} = result
|
||||
total[type] = Number(count)*10e5
|
||||
console.debug(`metrics/compute/plugins > achievements > set total.${type} to ${total[type]}`)
|
||||
}
|
||||
await page.close()
|
||||
await imports.wait(10*Math.random())
|
||||
}
|
||||
//Check setup state
|
||||
if ((!total.users)||(!total.repositories))
|
||||
return reject("Failed to initiate total for achievement plugin")
|
||||
console.debug("metrics/compute/plugins > achievements > total setup complete")
|
||||
return solve()
|
||||
})
|
||||
}
|
||||
return total.promise
|
||||
async function total({imports}) {
|
||||
if (!total.promise) {
|
||||
total.promise = new Promise(async (solve, reject) => {
|
||||
//Setup browser
|
||||
console.debug("metrics/compute/plugins > achievements > filling total from github.com/search")
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/plugins > achievements > started ${await browser.version()}`)
|
||||
//Extracting total from github.com/search
|
||||
for (let i = 0; (i < 100) && ((!total.users) || (!total.repositories)); i++) {
|
||||
const page = await browser.newPage()
|
||||
await page.goto("https://github.com/search")
|
||||
const result = await page.evaluate(() => [...document.querySelectorAll("h2")].filter(node => /Search more/.test(node.innerText)).shift()?.innerText.trim().match(/(?<count>\d+)M\s+(?<type>repositories|users|issues)$/)?.groups) ?? null
|
||||
console.log(`metrics/compute/plugins > achievements > setup found ${result?.type ?? "(?)"}`)
|
||||
if ((result?.type) && (!total[result.type])) {
|
||||
const {count, type} = result
|
||||
total[type] = Number(count) * 10e5
|
||||
console.debug(`metrics/compute/plugins > achievements > set total.${type} to ${total[type]}`)
|
||||
}
|
||||
await page.close()
|
||||
await imports.wait(10 * Math.random())
|
||||
}
|
||||
//Check setup state
|
||||
if ((!total.users) || (!total.repositories))
|
||||
return reject("Failed to initiate total for achievement plugin")
|
||||
console.debug("metrics/compute/plugins > achievements > total setup complete")
|
||||
return solve()
|
||||
})
|
||||
}
|
||||
return total.promise
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
//Exports
|
||||
export {default as user} from "./users.mjs"
|
||||
export {default as organization} from "./organizations.mjs"
|
||||
export {default as organization} from "./organizations.mjs"
|
||||
export {default as user} from "./users.mjs"
|
||||
|
||||
@@ -1,140 +1,158 @@
|
||||
/**Achievements list for users accounts */
|
||||
export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) {
|
||||
|
||||
//Initialization
|
||||
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 ranks = await graphql(queries.achievements.ranking(scores))
|
||||
const requirements = {stars:5, followers:3, forks:1, created:1}
|
||||
|
||||
//Developers
|
||||
{
|
||||
const value = organization.repositories.totalCount
|
||||
const unlock = organization.repositories.nodes?.shift()
|
||||
list.push({
|
||||
title:"Developers",
|
||||
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>",
|
||||
...rank(value, [1, 50, 100, 200]), value, unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.created_rank.userCount, requirement:scores.created >= requirements.created, type:"users"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Forkers
|
||||
{
|
||||
const value = organization.forks.totalCount
|
||||
const unlock = organization.forks.nodes?.shift()
|
||||
list.push({
|
||||
title:"Forkers",
|
||||
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>",
|
||||
...rank(value, [1, 10, 30, 50]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Managers
|
||||
{
|
||||
const value = organization.projects.totalCount
|
||||
const unlock = organization.projects.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Managers",
|
||||
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>",
|
||||
...rank(value, [1, 2, 4, 8]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Packagers
|
||||
{
|
||||
const value = organization.packages.totalCount
|
||||
const unlock = organization.packages.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Packagers",
|
||||
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>",
|
||||
...rank(value, [1, 20, 50, 100]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Maintainers
|
||||
{
|
||||
const value = organization.popular.nodes?.shift()?.stargazers?.totalCount ?? 0
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Maintainers",
|
||||
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>",
|
||||
...rank(value, [1, 5000, 10000, 30000]), value, unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.repo_rank.repositoryCount, requirement:scores.stars >= requirements.stars, type:"repositories"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Inspirationers
|
||||
{
|
||||
const value = Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount))
|
||||
const unlock = null
|
||||
list.push({
|
||||
title:"Inspirationers",
|
||||
text:`Maintaining 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>",
|
||||
...rank(value, [1, 500, 1000, 3000]), value, unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.forks_rank.repositoryCount, requirement:scores.forks >= requirements.forks, type:"repositories"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Polyglots
|
||||
{
|
||||
const value = new Set(data.user.repositories.nodes.flatMap(repository => repository.languages.edges.map(({node:{name}}) => name))).size
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Polyglots",
|
||||
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>",
|
||||
...rank(value, [1, 8, 16, 32]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Sponsors
|
||||
{
|
||||
const value = organization.sponsorshipsAsSponsor.totalCount
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Sponsors",
|
||||
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>",
|
||||
...rank(value, [1, 5, 10, 20]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Organization
|
||||
{
|
||||
const value = organization.membersWithRole.totalCount
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Organization",
|
||||
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>",
|
||||
...rank(value, [1, 100, 500, 1000]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Member
|
||||
{
|
||||
const value = computed.registered.diff
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Member",
|
||||
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>",
|
||||
...rank(value, [1, 3, 5, 10]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) {
|
||||
//Initialization
|
||||
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 ranks = await graphql(queries.achievements.ranking(scores))
|
||||
const requirements = {stars:5, followers:3, forks:1, created:1}
|
||||
|
||||
//Developers
|
||||
{
|
||||
const value = organization.repositories.totalCount
|
||||
const unlock = organization.repositories.nodes?.shift()
|
||||
list.push({
|
||||
title:"Developers",
|
||||
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>',
|
||||
...rank(value, [1, 50, 100, 200]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.created_rank.userCount, requirement:scores.created >= requirements.created, type:"users"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Forkers
|
||||
{
|
||||
const value = organization.forks.totalCount
|
||||
const unlock = organization.forks.nodes?.shift()
|
||||
list.push({
|
||||
title:"Forkers",
|
||||
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>',
|
||||
...rank(value, [1, 10, 30, 50]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Managers
|
||||
{
|
||||
const value = organization.projects.totalCount
|
||||
const unlock = organization.projects.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Managers",
|
||||
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>',
|
||||
...rank(value, [1, 2, 4, 8]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Packagers
|
||||
{
|
||||
const value = organization.packages.totalCount
|
||||
const unlock = organization.packages.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Packagers",
|
||||
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>',
|
||||
...rank(value, [1, 20, 50, 100]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Maintainers
|
||||
{
|
||||
const value = organization.popular.nodes?.shift()?.stargazers?.totalCount ?? 0
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Maintainers",
|
||||
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>',
|
||||
...rank(value, [1, 5000, 10000, 30000]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.repo_rank.repositoryCount, requirement:scores.stars >= requirements.stars, type:"repositories"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Inspirationers
|
||||
{
|
||||
const value = Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount))
|
||||
const unlock = null
|
||||
list.push({
|
||||
title:"Inspirationers",
|
||||
text:`Maintaining 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>',
|
||||
...rank(value, [1, 500, 1000, 3000]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.forks_rank.repositoryCount, requirement:scores.forks >= requirements.forks, type:"repositories"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Polyglots
|
||||
{
|
||||
const value = new Set(data.user.repositories.nodes.flatMap(repository => repository.languages.edges.map(({node:{name}}) => name))).size
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Polyglots",
|
||||
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>',
|
||||
...rank(value, [1, 8, 16, 32]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Sponsors
|
||||
{
|
||||
const value = organization.sponsorshipsAsSponsor.totalCount
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Sponsors",
|
||||
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>',
|
||||
...rank(value, [1, 5, 10, 20]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Organization
|
||||
{
|
||||
const value = organization.membersWithRole.totalCount
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Organization",
|
||||
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>',
|
||||
...rank(value, [1, 100, 500, 1000]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Member
|
||||
{
|
||||
const value = computed.registered.diff
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Member",
|
||||
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>',
|
||||
...rank(value, [1, 3, 5, 10]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,284 +1,329 @@
|
||||
/**Achievements list for users accounts */
|
||||
export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) {
|
||||
|
||||
//Initialization
|
||||
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 ranks = await graphql(queries.achievements.ranking(scores))
|
||||
const requirements = {stars:5, followers:3, forks:1, created:1}
|
||||
|
||||
//Developer
|
||||
{
|
||||
const value = user.repositories.totalCount
|
||||
const unlock = user.repositories.nodes?.shift()
|
||||
list.push({
|
||||
title:"Developer",
|
||||
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>",
|
||||
...rank(value, [1, 20, 50, 100]), value, unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.created_rank.userCount, requirement:scores.created >= requirements.created, type:"users"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Forker
|
||||
{
|
||||
const value = user.forks.totalCount
|
||||
const unlock = user.forks.nodes?.shift()
|
||||
list.push({
|
||||
title:"Forker",
|
||||
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>",
|
||||
...rank(value, [1, 5, 10, 20]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Contributor
|
||||
{
|
||||
const value = user.pullRequests.totalCount
|
||||
const unlock = user.pullRequests.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Contributor",
|
||||
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>",
|
||||
...rank(value, [1, 200, 500, 1000]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Manager
|
||||
{
|
||||
const value = user.projects.totalCount
|
||||
const unlock = user.projects.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Manager",
|
||||
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>",
|
||||
...rank(value, [1, 2, 3, 4]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Reviewer
|
||||
{
|
||||
const value = user.contributionsCollection.pullRequestReviewContributions.totalCount
|
||||
const unlock = user.contributionsCollection.pullRequestReviewContributions.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Reviewer",
|
||||
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>",
|
||||
...rank(value, [1, 200, 500, 1000]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Packager
|
||||
{
|
||||
const value = user.packages.totalCount
|
||||
const unlock = user.packages.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Packager",
|
||||
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>",
|
||||
...rank(value, [1, 5, 10, 20]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Scripter
|
||||
{
|
||||
const value = user.gists.totalCount
|
||||
const unlock = user.gists.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Scripter",
|
||||
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>",
|
||||
...rank(value, [1, 20, 50, 100]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Worker
|
||||
{
|
||||
const value = user.organizations.totalCount
|
||||
const unlock = user.organizations.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Worker",
|
||||
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>",
|
||||
...rank(value, [1, 2, 4, 8]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Stargazer
|
||||
{
|
||||
const value = user.starredRepositories.totalCount
|
||||
const unlock = user.starredRepositories.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Stargazer",
|
||||
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>",
|
||||
...rank(value, [1, 200, 500, 1000]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Follower
|
||||
{
|
||||
const value = user.following.totalCount
|
||||
const unlock = user.following.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Follower",
|
||||
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>",
|
||||
...rank(value, [1, 200, 500, 1000]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Influencer
|
||||
{
|
||||
const value = user.followers.totalCount
|
||||
const unlock = user.followers.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Influencer",
|
||||
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>",
|
||||
...rank(value, [1, 200, 500, 1000]), value, unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.user_rank.userCount, requirement:scores.followers >= requirements.followers, type:"users"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Maintainer
|
||||
{
|
||||
const value = user.popular.nodes?.shift()?.stargazers?.totalCount ?? 0
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Maintainer",
|
||||
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>",
|
||||
...rank(value, [1, 1000, 5000, 10000]), value, unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.repo_rank.repositoryCount, requirement:scores.stars >= requirements.stars, type:"repositories"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Inspirationer
|
||||
{
|
||||
const value = Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount))
|
||||
const unlock = null
|
||||
list.push({
|
||||
title:"Inspirationer",
|
||||
text:`Maintaining 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>",
|
||||
...rank(value, [1, 100, 500, 1000]), value, unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.forks_rank.repositoryCount, requirement:scores.forks >= requirements.forks, type:"repositories"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Polyglot
|
||||
{
|
||||
const value = new Set(data.user.repositories.nodes.flatMap(repository => repository.languages.edges.map(({node:{name}}) => name))).size
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Polyglot",
|
||||
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>",
|
||||
...rank(value, [1, 4, 8, 16]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Member
|
||||
{
|
||||
const value = computed.registered.diff
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Member",
|
||||
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>",
|
||||
...rank(value, [1, 3, 5, 10]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Sponsors
|
||||
{
|
||||
const value = user.sponsorshipsAsSponsor.totalCount
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Sponsor",
|
||||
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>",
|
||||
...rank(value, [1, 3, 5, 10]), value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Verified
|
||||
{
|
||||
const value = !/This user hasn't uploaded any GPG keys/i.test((await imports.axios.get(`https://github.com/${login}.gpg`)).data)
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Verified",
|
||||
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>",
|
||||
rank:value ? "$" : "X", progress:value ? 1 : 0, value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Explorer
|
||||
{
|
||||
const value = !/doesn’t have any starred topics yet/i.test((await imports.axios.get(`https://github.com/stars/${login}/topics`)).data)
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Explorer",
|
||||
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>",
|
||||
rank:value ? "$" : "X", progress:value ? 1 : 0, value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Automater
|
||||
{
|
||||
const value = process.env.GITHUB_ACTIONS
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Automater",
|
||||
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>",
|
||||
rank:value ? "$" : "X", progress:value ? 1 : 0, value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Infographile
|
||||
{
|
||||
const {repository:{viewerHasStarred:value}, viewer:{login:_login}} = await graphql(queries.achievements.metrics())
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Infographile",
|
||||
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>",
|
||||
rank:(value)&&(login === _login) ? "$" : "X", progress:(value)&&(login === _login) ? 1 : 0, value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Octonaut
|
||||
{
|
||||
const {user:{viewerIsFollowing:value}, viewer:{login:_login}} = await graphql(queries.achievements.octocat())
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Octonaut",
|
||||
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>",
|
||||
rank:(value)&&(login === _login) ? "$" : "X", progress:(value)&&(login === _login) ? 1 : 0, value, unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) {
|
||||
//Initialization
|
||||
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 ranks = await graphql(queries.achievements.ranking(scores))
|
||||
const requirements = {stars:5, followers:3, forks:1, created:1}
|
||||
|
||||
//Developer
|
||||
{
|
||||
const value = user.repositories.totalCount
|
||||
const unlock = user.repositories.nodes?.shift()
|
||||
list.push({
|
||||
title:"Developer",
|
||||
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>',
|
||||
...rank(value, [1, 20, 50, 100]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.created_rank.userCount, requirement:scores.created >= requirements.created, type:"users"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Forker
|
||||
{
|
||||
const value = user.forks.totalCount
|
||||
const unlock = user.forks.nodes?.shift()
|
||||
list.push({
|
||||
title:"Forker",
|
||||
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>',
|
||||
...rank(value, [1, 5, 10, 20]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Contributor
|
||||
{
|
||||
const value = user.pullRequests.totalCount
|
||||
const unlock = user.pullRequests.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Contributor",
|
||||
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>',
|
||||
...rank(value, [1, 200, 500, 1000]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Manager
|
||||
{
|
||||
const value = user.projects.totalCount
|
||||
const unlock = user.projects.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Manager",
|
||||
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>',
|
||||
...rank(value, [1, 2, 3, 4]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Reviewer
|
||||
{
|
||||
const value = user.contributionsCollection.pullRequestReviewContributions.totalCount
|
||||
const unlock = user.contributionsCollection.pullRequestReviewContributions.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Reviewer",
|
||||
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>',
|
||||
...rank(value, [1, 200, 500, 1000]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Packager
|
||||
{
|
||||
const value = user.packages.totalCount
|
||||
const unlock = user.packages.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Packager",
|
||||
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>',
|
||||
...rank(value, [1, 5, 10, 20]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Scripter
|
||||
{
|
||||
const value = user.gists.totalCount
|
||||
const unlock = user.gists.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Scripter",
|
||||
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>',
|
||||
...rank(value, [1, 20, 50, 100]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Worker
|
||||
{
|
||||
const value = user.organizations.totalCount
|
||||
const unlock = user.organizations.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Worker",
|
||||
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>',
|
||||
...rank(value, [1, 2, 4, 8]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Stargazer
|
||||
{
|
||||
const value = user.starredRepositories.totalCount
|
||||
const unlock = user.starredRepositories.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Stargazer",
|
||||
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>',
|
||||
...rank(value, [1, 200, 500, 1000]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Follower
|
||||
{
|
||||
const value = user.following.totalCount
|
||||
const unlock = user.following.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Follower",
|
||||
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>',
|
||||
...rank(value, [1, 200, 500, 1000]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Influencer
|
||||
{
|
||||
const value = user.followers.totalCount
|
||||
const unlock = user.followers.nodes?.shift()
|
||||
|
||||
list.push({
|
||||
title:"Influencer",
|
||||
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>',
|
||||
...rank(value, [1, 200, 500, 1000]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.user_rank.userCount, requirement:scores.followers >= requirements.followers, type:"users"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Maintainer
|
||||
{
|
||||
const value = user.popular.nodes?.shift()?.stargazers?.totalCount ?? 0
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Maintainer",
|
||||
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>',
|
||||
...rank(value, [1, 1000, 5000, 10000]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.repo_rank.repositoryCount, requirement:scores.stars >= requirements.stars, type:"repositories"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Inspirationer
|
||||
{
|
||||
const value = Math.max(0, ...data.user.repositories.nodes.map(({forkCount}) => forkCount))
|
||||
const unlock = null
|
||||
list.push({
|
||||
title:"Inspirationer",
|
||||
text:`Maintaining 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>',
|
||||
...rank(value, [1, 100, 500, 1000]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
leaderboard:leaderboard({user:ranks.forks_rank.repositoryCount, requirement:scores.forks >= requirements.forks, type:"repositories"}),
|
||||
})
|
||||
}
|
||||
|
||||
//Polyglot
|
||||
{
|
||||
const value = new Set(data.user.repositories.nodes.flatMap(repository => repository.languages.edges.map(({node:{name}}) => name))).size
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Polyglot",
|
||||
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>',
|
||||
...rank(value, [1, 4, 8, 16]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Member
|
||||
{
|
||||
const value = computed.registered.diff
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Member",
|
||||
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>',
|
||||
...rank(value, [1, 3, 5, 10]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Sponsors
|
||||
{
|
||||
const value = user.sponsorshipsAsSponsor.totalCount
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Sponsor",
|
||||
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>',
|
||||
...rank(value, [1, 3, 5, 10]),
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Verified
|
||||
{
|
||||
const value = !/This user hasn't uploaded any GPG keys/i.test((await imports.axios.get(`https://github.com/${login}.gpg`)).data)
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Verified",
|
||||
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>',
|
||||
rank:value ? "$" : "X",
|
||||
progress:value ? 1 : 0,
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Explorer
|
||||
{
|
||||
const value = !/doesn’t have any starred topics yet/i.test((await imports.axios.get(`https://github.com/stars/${login}/topics`)).data)
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Explorer",
|
||||
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>',
|
||||
rank:value ? "$" : "X",
|
||||
progress:value ? 1 : 0,
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Automater
|
||||
{
|
||||
const value = process.env.GITHUB_ACTIONS
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Automater",
|
||||
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>',
|
||||
rank:value ? "$" : "X",
|
||||
progress:value ? 1 : 0,
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Infographile
|
||||
{
|
||||
const {repository:{viewerHasStarred:value}, viewer:{login:_login}} = await graphql(queries.achievements.metrics())
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Infographile",
|
||||
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>',
|
||||
rank:(value) && (login === _login) ? "$" : "X",
|
||||
progress:(value) && (login === _login) ? 1 : 0,
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
|
||||
//Octonaut
|
||||
{
|
||||
const {user:{viewerIsFollowing:value}, viewer:{login:_login}} = await graphql(queries.achievements.octocat())
|
||||
const unlock = null
|
||||
|
||||
list.push({
|
||||
title:"Octonaut",
|
||||
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>',
|
||||
rank:(value) && (login === _login) ? "$" : "X",
|
||||
progress:(value) && (login === _login) ? 1 : 0,
|
||||
value,
|
||||
unlock:new Date(unlock?.createdAt),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,148 +1,150 @@
|
||||
//Setup
|
||||
export default async function({login, data, rest, q, account, imports}, {enabled = false, markdown = "inline"} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.activity))
|
||||
export default async function({login, data, rest, q, account, imports}, {enabled = false, markdown = "inline"} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.activity))
|
||||
return null
|
||||
|
||||
//Context
|
||||
let context = {mode:"user"}
|
||||
if (q.repo) {
|
||||
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()
|
||||
context = {...context, mode:"repository", owner, repo}
|
||||
}
|
||||
|
||||
//Load inputs
|
||||
let {limit, days, filter, visibility, timestamps, skipped} = imports.metadata.plugins.activity.inputs({data, q, account})
|
||||
if (!days)
|
||||
days = Infinity
|
||||
skipped.push(...data.shared["repositories.skipped"])
|
||||
const codelines = 2
|
||||
|
||||
//Get user recent activity
|
||||
console.debug(`metrics/compute/${login}/plugins > activity > querying api`)
|
||||
const {data:events} = context.mode === "repository" ? await rest.activity.listRepoEvents({owner:context.owner, repo:context.repo}) : await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100})
|
||||
console.debug(`metrics/compute/${login}/plugins > activity > ${events.length} events loaded`)
|
||||
|
||||
//Extract activity events
|
||||
const activity = (await Promise.all(
|
||||
events
|
||||
.filter(({actor}) => account === "organization" ? true : actor.login === login)
|
||||
.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)
|
||||
.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
|
||||
const timestamp = new Date(created_at)
|
||||
if ((skipped.includes(repo.split("/").pop())) || (skipped.includes(repo)))
|
||||
return null
|
||||
|
||||
//Context
|
||||
let context = {mode:"user"}
|
||||
if (q.repo) {
|
||||
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()
|
||||
context = {...context, mode:"repository", owner, repo}
|
||||
switch (type) {
|
||||
//Commented on a commit
|
||||
case "CommitCommentEvent": {
|
||||
if (!["created"].includes(payload.action))
|
||||
return null
|
||||
const {comment:{user:{login:user}, commit_id:sha, body:content}} = payload
|
||||
return {type:"comment", on:"commit", 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
|
||||
case "CreateEvent": {
|
||||
const {ref:name, ref_type:type} = payload
|
||||
return {type:"ref/create", actor, timestamp, repo, ref:{name, type}}
|
||||
}
|
||||
//Deleted a git branch or tag
|
||||
case "DeleteEvent": {
|
||||
const {ref:name, ref_type:type} = payload
|
||||
return {type:"ref/delete", actor, timestamp, repo, ref:{name, type}}
|
||||
}
|
||||
//Forked repository
|
||||
case "ForkEvent": {
|
||||
return {type:"fork", actor, timestamp, repo}
|
||||
}
|
||||
//Wiki editions
|
||||
case "GollumEvent": {
|
||||
const {pages} = payload
|
||||
return {type:"wiki", actor, timestamp, repo, pages:pages.map(({title}) => title)}
|
||||
}
|
||||
//Commented on an issue
|
||||
case "IssueCommentEvent": {
|
||||
if (!["created"].includes(payload.action))
|
||||
return null
|
||||
const {issue:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload
|
||||
return {type:"comment", on:"issue", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile, number, title}
|
||||
}
|
||||
//Issue event
|
||||
case "IssuesEvent": {
|
||||
if (!["opened", "closed", "reopened"].includes(payload.action))
|
||||
return null
|
||||
const {action, issue:{user:{login:user}, title, number, body:content}} = payload
|
||||
return {type:"issue", actor, timestamp, repo, action, user, number, title, content:await imports.markdown(content, {mode:markdown, codelines})}
|
||||
}
|
||||
//Activity from repository collaborators
|
||||
case "MemberEvent": {
|
||||
if (!["added"].includes(payload.action))
|
||||
return null
|
||||
const {member:{login:user}} = payload
|
||||
return {type:"member", actor, timestamp, repo, user}
|
||||
}
|
||||
//Made repository public
|
||||
case "PublicEvent": {
|
||||
return {type:"public", actor, timestamp, repo}
|
||||
}
|
||||
//Pull requests events
|
||||
case "PullRequestEvent": {
|
||||
if (!["opened", "closed"].includes(payload.action))
|
||||
return null
|
||||
const {action, pull_request:{user:{login:user}, title, number, body:content, additions:added, deletions:deleted, changed_files:changed, merged}} = payload
|
||||
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
|
||||
case "PullRequestReviewEvent": {
|
||||
const {review:{state:review}, pull_request:{user:{login:user}, number, title}} = payload
|
||||
return {type:"review", actor, timestamp, repo, review, user, number, title}
|
||||
}
|
||||
//Commented on a pull request
|
||||
case "PullRequestReviewCommentEvent": {
|
||||
if (!["created"].includes(payload.action))
|
||||
return null
|
||||
const {pull_request:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload
|
||||
return {type:"comment", on:"pr", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile, number, title}
|
||||
}
|
||||
//Pushed commits
|
||||
case "PushEvent": {
|
||||
let {size, commits, ref} = payload
|
||||
if (commits[commits.length - 1].message.startsWith("Merge branch "))
|
||||
commits = [commits[commits.length - 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}))}
|
||||
}
|
||||
//Released
|
||||
case "ReleaseEvent": {
|
||||
if (!["published"].includes(payload.action))
|
||||
return null
|
||||
const {action, release:{name, prerelease, draft, body:content}} = payload
|
||||
return {type:"release", actor, timestamp, repo, action, name, prerelease, draft, content:await imports.markdown(content, {mode:markdown, codelines})}
|
||||
}
|
||||
//Starred a repository
|
||||
case "WatchEvent": {
|
||||
if (!["started"].includes(payload.action))
|
||||
return null
|
||||
const {action} = payload
|
||||
return {type:"star", actor, timestamp, repo, action}
|
||||
}
|
||||
//Unknown event
|
||||
default: {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}),
|
||||
))
|
||||
.filter(event => event)
|
||||
.filter(event => filter.includes("all") || filter.includes(event.type))
|
||||
.slice(0, limit)
|
||||
|
||||
//Load inputs
|
||||
let {limit, days, filter, visibility, timestamps, skipped} = imports.metadata.plugins.activity.inputs({data, q, account})
|
||||
if (!days)
|
||||
days = Infinity
|
||||
skipped.push(...data.shared["repositories.skipped"])
|
||||
const codelines = 2
|
||||
|
||||
//Get user recent activity
|
||||
console.debug(`metrics/compute/${login}/plugins > activity > querying api`)
|
||||
const {data:events} = context.mode === "repository" ? await rest.activity.listRepoEvents({owner:context.owner, repo:context.repo}) : await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100})
|
||||
console.debug(`metrics/compute/${login}/plugins > activity > ${events.length} events loaded`)
|
||||
|
||||
//Extract activity events
|
||||
const activity = (await Promise.all(events
|
||||
.filter(({actor}) => account === "organization" ? true : actor.login === login)
|
||||
.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)
|
||||
.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
|
||||
const timestamp = new Date(created_at)
|
||||
if ((skipped.includes(repo.split("/").pop()))||(skipped.includes(repo)))
|
||||
return null
|
||||
switch (type) {
|
||||
//Commented on a commit
|
||||
case "CommitCommentEvent":{
|
||||
if (!["created"].includes(payload.action))
|
||||
return null
|
||||
const {comment:{user:{login:user}, commit_id:sha, body:content}} = payload
|
||||
return {type:"comment", on:"commit", 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
|
||||
case "CreateEvent":{
|
||||
const {ref:name, ref_type:type} = payload
|
||||
return {type:"ref/create", actor, timestamp, repo, ref:{name, type}}
|
||||
}
|
||||
//Deleted a git branch or tag
|
||||
case "DeleteEvent":{
|
||||
const {ref:name, ref_type:type} = payload
|
||||
return {type:"ref/delete", actor, timestamp, repo, ref:{name, type}}
|
||||
}
|
||||
//Forked repository
|
||||
case "ForkEvent":{
|
||||
return {type:"fork", actor, timestamp, repo}
|
||||
}
|
||||
//Wiki editions
|
||||
case "GollumEvent":{
|
||||
const {pages} = payload
|
||||
return {type:"wiki", actor, timestamp, repo, pages:pages.map(({title}) => title)}
|
||||
}
|
||||
//Commented on an issue
|
||||
case "IssueCommentEvent":{
|
||||
if (!["created"].includes(payload.action))
|
||||
return null
|
||||
const {issue:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload
|
||||
return {type:"comment", on:"issue", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile, number, title}
|
||||
}
|
||||
//Issue event
|
||||
case "IssuesEvent":{
|
||||
if (!["opened", "closed", "reopened"].includes(payload.action))
|
||||
return null
|
||||
const {action, issue:{user:{login:user}, title, number, body:content}} = payload
|
||||
return {type:"issue", actor, timestamp, repo, action, user, number, title, content:await imports.markdown(content, {mode:markdown, codelines})}
|
||||
}
|
||||
//Activity from repository collaborators
|
||||
case "MemberEvent":{
|
||||
if (!["added"].includes(payload.action))
|
||||
return null
|
||||
const {member:{login:user}} = payload
|
||||
return {type:"member", actor, timestamp, repo, user}
|
||||
}
|
||||
//Made repository public
|
||||
case "PublicEvent":{
|
||||
return {type:"public", actor, timestamp, repo}
|
||||
}
|
||||
//Pull requests events
|
||||
case "PullRequestEvent":{
|
||||
if (!["opened", "closed"].includes(payload.action))
|
||||
return null
|
||||
const {action, pull_request:{user:{login:user}, title, number, body:content, additions:added, deletions:deleted, changed_files:changed, merged}} = payload
|
||||
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
|
||||
case "PullRequestReviewEvent":{
|
||||
const {review:{state:review}, pull_request:{user:{login:user}, number, title}} = payload
|
||||
return {type:"review", actor, timestamp, repo, review, user, number, title}
|
||||
}
|
||||
//Commented on a pull request
|
||||
case "PullRequestReviewCommentEvent":{
|
||||
if (!["created"].includes(payload.action))
|
||||
return null
|
||||
const {pull_request:{user:{login:user}, title, number}, comment:{body:content, performed_via_github_app:mobile}} = payload
|
||||
return {type:"comment", on:"pr", actor, timestamp, repo, content:await imports.markdown(content, {mode:markdown, codelines}), user, mobile, number, title}
|
||||
}
|
||||
//Pushed commits
|
||||
case "PushEvent":{
|
||||
let {size, commits, ref} = payload
|
||||
if (commits[commits.length-1].message.startsWith("Merge branch "))
|
||||
commits = [commits[commits.length-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}))}
|
||||
}
|
||||
//Released
|
||||
case "ReleaseEvent":{
|
||||
if (!["published"].includes(payload.action))
|
||||
return null
|
||||
const {action, release:{name, prerelease, draft, body:content}} = payload
|
||||
return {type:"release", actor, timestamp, repo, action, name, prerelease, draft, content:await imports.markdown(content, {mode:markdown, codelines})}
|
||||
}
|
||||
//Starred a repository
|
||||
case "WatchEvent":{
|
||||
if (!["started"].includes(payload.action))
|
||||
return null
|
||||
const {action} = payload
|
||||
return {type:"star", actor, timestamp, repo, action}
|
||||
}
|
||||
//Unknown event
|
||||
default:{
|
||||
return null
|
||||
}
|
||||
}
|
||||
})))
|
||||
.filter(event => event)
|
||||
.filter(event => filter.includes("all") || filter.includes(event.type))
|
||||
.slice(0, limit)
|
||||
|
||||
//Results
|
||||
return {timestamps, events:activity}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {timestamps, events:activity}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,158 +1,162 @@
|
||||
//Setup
|
||||
export default async function({login, data, queries, imports, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
export default async function({login, data, queries, imports, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.anilist))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {limit, "limit.characters":limit_characters, medias, sections, shuffle, user} = imports.metadata.plugins.anilist.inputs({data, account, q})
|
||||
|
||||
//Initialization
|
||||
const result = {user:{stats:null, genres:[]}, lists:Object.fromEntries(medias.map(type => [type, {}])), characters:[], sections}
|
||||
|
||||
//User statistics
|
||||
for (let retried = false; !retried; retried = true) {
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.anilist))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {limit, "limit.characters":limit_characters, medias, sections, shuffle, user} = imports.metadata.plugins.anilist.inputs({data, account, q})
|
||||
|
||||
//Initialization
|
||||
const result = {user:{stats:null, genres:[]}, lists:Object.fromEntries(medias.map(type => [type, {}])), characters:[], sections}
|
||||
|
||||
//User statistics
|
||||
for (let retried = false; !retried; retried = true) {
|
||||
try {
|
||||
//Query API
|
||||
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()})
|
||||
//Format and save results
|
||||
result.user.stats = stats
|
||||
result.user.genres = [...new Set([...stats.anime.genres.map(({genre}) => genre), ...stats.manga.genres.map(({genre}) => genre)])]
|
||||
}
|
||||
catch (error) {
|
||||
await retry({login, imports, error})
|
||||
}
|
||||
}
|
||||
|
||||
//Medias lists
|
||||
if ((sections.includes("watching"))||(sections.includes("reading"))) {
|
||||
for (const type of medias) {
|
||||
for (let retried = false; !retried; retried = true) {
|
||||
try {
|
||||
//Query API
|
||||
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()})
|
||||
//Format and save results
|
||||
for (const {name, entries} of lists) {
|
||||
//Format results
|
||||
const list = await Promise.all(entries.map(media => format({media, imports})))
|
||||
result.lists[type][name.toLocaleLowerCase()] = shuffle ? imports.shuffle(list) : list
|
||||
//Limit results
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > keeping only ${limit} medias`)
|
||||
result.lists[type][name.toLocaleLowerCase()].splice(limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
await retry({login, imports, error})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Favorites anime/manga
|
||||
if (sections.includes("favorites")) {
|
||||
for (const type of medias) {
|
||||
//Query API
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (favorites ${type}s)`)
|
||||
const list = []
|
||||
let page = 1
|
||||
let next = false
|
||||
do {
|
||||
try {
|
||||
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})})
|
||||
page++
|
||||
next = cursor.hasNextPage
|
||||
list.push(...await Promise.all(nodes.map(media => format({media:{progess:null, score:null, media}, imports}))))
|
||||
}
|
||||
catch (error) {
|
||||
if (await retry({login, imports, error}))
|
||||
continue
|
||||
}
|
||||
} while (next)
|
||||
//Format and save results
|
||||
result.lists[type].favorites = shuffle ? imports.shuffle(list) : list
|
||||
//Limit results
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > keeping only ${limit} medias`)
|
||||
result.lists[type].favorites.splice(limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Favorites characters
|
||||
if (sections.includes("characters")) {
|
||||
//Query API
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (favorites characters)`)
|
||||
const characters = []
|
||||
let page = 1
|
||||
let next = false
|
||||
do {
|
||||
try {
|
||||
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()})
|
||||
page++
|
||||
next = cursor.hasNextPage
|
||||
for (const {name:{full:name}, image:{medium:artwork}} of nodes) {
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > processing ${name}`)
|
||||
characters.push({name, artwork:await imports.imgb64(artwork)})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (await retry({login, imports, error}))
|
||||
continue
|
||||
}
|
||||
} while (next)
|
||||
//Format and save results
|
||||
result.characters = shuffle ? imports.shuffle(characters) : characters
|
||||
//Limit results
|
||||
if (limit_characters > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > keeping only ${limit_characters} characters`)
|
||||
result.characters.splice(limit_characters)
|
||||
}
|
||||
}
|
||||
|
||||
//Results
|
||||
return result
|
||||
//Query API
|
||||
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()})
|
||||
//Format and save results
|
||||
result.user.stats = stats
|
||||
result.user.genres = [...new Set([...stats.anime.genres.map(({genre}) => genre), ...stats.manga.genres.map(({genre}) => genre)])]
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
console.log(error.response.data)
|
||||
message = `API returned ${status}`
|
||||
error = error.response?.data ?? null
|
||||
catch (error) {
|
||||
await retry({login, imports, error})
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
|
||||
//Medias lists
|
||||
if ((sections.includes("watching")) || (sections.includes("reading"))) {
|
||||
for (const type of medias) {
|
||||
for (let retried = false; !retried; retried = true) {
|
||||
try {
|
||||
//Query API
|
||||
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()})
|
||||
//Format and save results
|
||||
for (const {name, entries} of lists) {
|
||||
//Format results
|
||||
const list = await Promise.all(entries.map(media => format({media, imports})))
|
||||
result.lists[type][name.toLocaleLowerCase()] = shuffle ? imports.shuffle(list) : list
|
||||
//Limit results
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > keeping only ${limit} medias`)
|
||||
result.lists[type][name.toLocaleLowerCase()].splice(limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
await retry({login, imports, error})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Favorites anime/manga
|
||||
if (sections.includes("favorites")) {
|
||||
for (const type of medias) {
|
||||
//Query API
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (favorites ${type}s)`)
|
||||
const list = []
|
||||
let page = 1
|
||||
let next = false
|
||||
do {
|
||||
try {
|
||||
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})})
|
||||
page++
|
||||
next = cursor.hasNextPage
|
||||
list.push(...await Promise.all(nodes.map(media => format({media:{progess:null, score:null, media}, imports}))))
|
||||
}
|
||||
catch (error) {
|
||||
if (await retry({login, imports, error}))
|
||||
continue
|
||||
}
|
||||
} while (next)
|
||||
//Format and save results
|
||||
result.lists[type].favorites = shuffle ? imports.shuffle(list) : list
|
||||
//Limit results
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > keeping only ${limit} medias`)
|
||||
result.lists[type].favorites.splice(limit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Favorites characters
|
||||
if (sections.includes("characters")) {
|
||||
//Query API
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > querying api (favorites characters)`)
|
||||
const characters = []
|
||||
let page = 1
|
||||
let next = false
|
||||
do {
|
||||
try {
|
||||
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()})
|
||||
page++
|
||||
next = cursor.hasNextPage
|
||||
for (const {name:{full:name}, image:{medium:artwork}} of nodes) {
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > processing ${name}`)
|
||||
characters.push({name, artwork:await imports.imgb64(artwork)})
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
if (await retry({login, imports, error}))
|
||||
continue
|
||||
}
|
||||
} while (next)
|
||||
//Format and save results
|
||||
result.characters = shuffle ? imports.shuffle(characters) : characters
|
||||
//Limit results
|
||||
if (limit_characters > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > keeping only ${limit_characters} characters`)
|
||||
result.characters.splice(limit_characters)
|
||||
}
|
||||
}
|
||||
|
||||
//Results
|
||||
return result
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
console.log(error.response.data)
|
||||
message = `API returned ${status}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
/**Media formatter */
|
||||
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
|
||||
return {
|
||||
name:title.romaji,
|
||||
type, status, release, genres, progress,
|
||||
description:description.replace(/<br\s*\\?>/g, " "),
|
||||
scores:{user:userScore, community:averageScore},
|
||||
released:type === "ANIME" ? episodes : chapters,
|
||||
artwork:await imports.imgb64(artwork),
|
||||
}
|
||||
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
|
||||
return {
|
||||
name:title.romaji,
|
||||
type,
|
||||
status,
|
||||
release,
|
||||
genres,
|
||||
progress,
|
||||
description:description.replace(/<br\s*\\?>/g, " "),
|
||||
scores:{user:userScore, community:averageScore},
|
||||
released:type === "ANIME" ? episodes : chapters,
|
||||
artwork:await imports.imgb64(artwork),
|
||||
}
|
||||
}
|
||||
|
||||
/**Rate-limiter handler */
|
||||
async function retry({login, imports, error}) {
|
||||
if ((error.isAxiosError)&&(error.response.status === 429)) {
|
||||
const delay = Number(error.response.headers["retry-after"])+5
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > reached requests limit, retrying in ${delay}s`)
|
||||
await imports.wait(delay)
|
||||
return true
|
||||
}
|
||||
throw error
|
||||
async function retry({login, imports, error}) {
|
||||
if ((error.isAxiosError) && (error.response.status === 429)) {
|
||||
const delay = Number(error.response.headers["retry-after"]) + 5
|
||||
console.debug(`metrics/compute/${login}/plugins > anilist > reached requests limit, retrying in ${delay}s`)
|
||||
await imports.wait(delay)
|
||||
return true
|
||||
}
|
||||
throw error
|
||||
}
|
||||
|
||||
@@ -4,132 +4,132 @@
|
||||
*/
|
||||
|
||||
//Setup
|
||||
export default async function({login, graphql, data, q, queries, imports}, conf) {
|
||||
//Load inputs
|
||||
console.debug(`metrics/compute/${login}/base > started`)
|
||||
let {repositories, "repositories.forks":_forks, "repositories.affiliations":_affiliations, "repositories.skipped":_skipped} = imports.metadata.plugins.base.inputs({data, q, account:"bypass"}, {repositories:conf.settings.repositories ?? 100})
|
||||
const forks = _forks ? "" : ", isFork: false"
|
||||
const affiliations = _affiliations?.length ? `, ownerAffiliations: [${_affiliations.map(x => x.toLocaleUpperCase()).join(", ")}]${conf.authenticated === login ? `, affiliations: [${_affiliations.map(x => x.toLocaleUpperCase()).join(", ")}]` : ""}` : ""
|
||||
export default async function({login, graphql, data, q, queries, imports}, conf) {
|
||||
//Load inputs
|
||||
console.debug(`metrics/compute/${login}/base > started`)
|
||||
let {repositories, "repositories.forks":_forks, "repositories.affiliations":_affiliations, "repositories.skipped":_skipped} = imports.metadata.plugins.base.inputs({data, q, account:"bypass"}, {repositories:conf.settings.repositories ?? 100})
|
||||
const forks = _forks ? "" : ", isFork: false"
|
||||
const affiliations = _affiliations?.length ? `, ownerAffiliations: [${_affiliations.map(x => x.toLocaleUpperCase()).join(", ")}]${conf.authenticated === login ? `, affiliations: [${_affiliations.map(x => x.toLocaleUpperCase()).join(", ")}]` : ""}` : ""
|
||||
|
||||
//Skip initial data gathering if not needed
|
||||
if (conf.settings.notoken)
|
||||
return (postprocess.skip({login, data}), {})
|
||||
//Skip initial data gathering if not needed
|
||||
if (conf.settings.notoken)
|
||||
return (postprocess.skip({login, data}), {})
|
||||
|
||||
//Base parts (legacy handling for web instance)
|
||||
const defaulted = ("base" in q) ? legacy.converter(q.base) ?? true : true
|
||||
for (const part of conf.settings.plugins.base.parts)
|
||||
data.base[part] = `base.${part}` in q ? legacy.converter(q[`base.${part}`]) : defaulted
|
||||
//Base parts (legacy handling for web instance)
|
||||
const defaulted = ("base" in q) ? legacy.converter(q.base) ?? true : true
|
||||
for (const part of conf.settings.plugins.base.parts)
|
||||
data.base[part] = `base.${part}` in q ? legacy.converter(q[`base.${part}`]) : defaulted
|
||||
|
||||
//Shared options
|
||||
data.shared = {"repositories.skipped":_skipped}
|
||||
console.debug(`metrics/compute/${login}/base > shared options > ${JSON.stringify(data.shared)}`)
|
||||
//Shared options
|
||||
data.shared = {"repositories.skipped":_skipped}
|
||||
console.debug(`metrics/compute/${login}/base > shared options > ${JSON.stringify(data.shared)}`)
|
||||
|
||||
//Iterate through account types
|
||||
for (const account of ["user", "organization"]) {
|
||||
try {
|
||||
//Query data from GitHub API
|
||||
console.debug(`metrics/compute/${login}/base > account ${account}`)
|
||||
const queried = await graphql(queries.base[account]({login, "calendar.from":new Date(Date.now()-14*24*60*60*1000).toISOString(), "calendar.to":(new Date()).toISOString(), forks, affiliations}))
|
||||
Object.assign(data, {user:queried[account]})
|
||||
postprocess?.[account]({login, data})
|
||||
//Query repositories from GitHub API
|
||||
{
|
||||
//Iterate through repositories
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
console.debug(`metrics/compute/${login}/base > retrieving repositories after ${cursor}`)
|
||||
const {[account]:{repositories:{edges, nodes}}} = await graphql(queries.base.repositories({login, account, after:cursor ? `after: "${cursor}"` : "", repositories:Math.min(repositories, {user:100, organization:25}[account]), forks, affiliations}))
|
||||
cursor = edges?.[edges?.length-1]?.cursor
|
||||
data.user.repositories.nodes.push(...nodes)
|
||||
pushed = nodes.length
|
||||
} while ((pushed)&&(cursor)&&(data.user.repositories.nodes.length < repositories))
|
||||
//Limit repositories
|
||||
console.debug(`metrics/compute/${login}/base > keeping only ${repositories} repositories`)
|
||||
data.user.repositories.nodes.splice(repositories)
|
||||
console.debug(`metrics/compute/${login}/base > loaded ${data.user.repositories.nodes.length} repositories`)
|
||||
}
|
||||
//Success
|
||||
console.debug(`metrics/compute/${login}/base > graphql query > account ${account} > success`)
|
||||
return {}
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(`metrics/compute/${login}/base > account ${account} > failed : ${error}`)
|
||||
if (/Could not resolve to a User with the login of/.test(error.message)) {
|
||||
console.debug(`metrics/compute/${login}/base > got a "user not found" error for account type "${account}" and user "${login}"`)
|
||||
console.debug(`metrics/compute/${login}/base > checking next account type`)
|
||||
continue
|
||||
}
|
||||
throw error
|
||||
}
|
||||
//Iterate through account types
|
||||
for (const account of ["user", "organization"]) {
|
||||
try {
|
||||
//Query data from GitHub API
|
||||
console.debug(`metrics/compute/${login}/base > account ${account}`)
|
||||
const queried = await graphql(queries.base[account]({login, "calendar.from":new Date(Date.now() - 14 * 24 * 60 * 60 * 1000).toISOString(), "calendar.to":(new Date()).toISOString(), forks, affiliations}))
|
||||
Object.assign(data, {user:queried[account]})
|
||||
postprocess?.[account]({login, data})
|
||||
//Query repositories from GitHub API
|
||||
{
|
||||
//Iterate through repositories
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
console.debug(`metrics/compute/${login}/base > retrieving repositories after ${cursor}`)
|
||||
const {[account]:{repositories:{edges, nodes}}} = await graphql(queries.base.repositories({login, account, after:cursor ? `after: "${cursor}"` : "", repositories:Math.min(repositories, {user:100, organization:25}[account]), forks, affiliations}))
|
||||
cursor = edges?.[edges?.length - 1]?.cursor
|
||||
data.user.repositories.nodes.push(...nodes)
|
||||
pushed = nodes.length
|
||||
} while ((pushed) && (cursor) && (data.user.repositories.nodes.length < repositories))
|
||||
//Limit repositories
|
||||
console.debug(`metrics/compute/${login}/base > keeping only ${repositories} repositories`)
|
||||
data.user.repositories.nodes.splice(repositories)
|
||||
console.debug(`metrics/compute/${login}/base > loaded ${data.user.repositories.nodes.length} repositories`)
|
||||
}
|
||||
//Not found
|
||||
console.debug(`metrics/compute/${login}/base > no more account type`)
|
||||
throw new Error("user not found")
|
||||
//Success
|
||||
console.debug(`metrics/compute/${login}/base > graphql query > account ${account} > success`)
|
||||
return {}
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(`metrics/compute/${login}/base > account ${account} > failed : ${error}`)
|
||||
if (/Could not resolve to a User with the login of/.test(error.message)) {
|
||||
console.debug(`metrics/compute/${login}/base > got a "user not found" error for account type "${account}" and user "${login}"`)
|
||||
console.debug(`metrics/compute/${login}/base > checking next account type`)
|
||||
continue
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
//Not found
|
||||
console.debug(`metrics/compute/${login}/base > no more account type`)
|
||||
throw new Error("user not found")
|
||||
}
|
||||
|
||||
//Query post-processing
|
||||
const postprocess = {
|
||||
//User
|
||||
user({login, data}) {
|
||||
console.debug(`metrics/compute/${login}/base > applying postprocessing`)
|
||||
data.account = "user"
|
||||
Object.assign(data.user, {
|
||||
isVerified:false,
|
||||
})
|
||||
const postprocess = {
|
||||
//User
|
||||
user({login, data}) {
|
||||
console.debug(`metrics/compute/${login}/base > applying postprocessing`)
|
||||
data.account = "user"
|
||||
Object.assign(data.user, {
|
||||
isVerified:false,
|
||||
})
|
||||
},
|
||||
//Organization
|
||||
organization({login, data}) {
|
||||
console.debug(`metrics/compute/${login}/base > applying postprocessing`)
|
||||
data.account = "organization"
|
||||
Object.assign(data.user, {
|
||||
isHireable:false,
|
||||
starredRepositories:{totalCount:0},
|
||||
watching:{totalCount:0},
|
||||
contributionsCollection:{
|
||||
totalRepositoriesWithContributedCommits:0,
|
||||
totalCommitContributions:0,
|
||||
restrictedContributionsCount:0,
|
||||
totalIssueContributions:0,
|
||||
totalPullRequestContributions:0,
|
||||
totalPullRequestReviewContributions:0,
|
||||
},
|
||||
//Organization
|
||||
organization({login, data}) {
|
||||
console.debug(`metrics/compute/${login}/base > applying postprocessing`)
|
||||
data.account = "organization"
|
||||
Object.assign(data.user, {
|
||||
isHireable:false,
|
||||
starredRepositories:{totalCount:0},
|
||||
watching:{totalCount:0},
|
||||
contributionsCollection:{
|
||||
totalRepositoriesWithContributedCommits:0,
|
||||
totalCommitContributions:0,
|
||||
restrictedContributionsCount:0,
|
||||
totalIssueContributions:0,
|
||||
totalPullRequestContributions:0,
|
||||
totalPullRequestReviewContributions:0,
|
||||
},
|
||||
calendar:{contributionCalendar:{weeks:[]}},
|
||||
repositoriesContributedTo:{totalCount:0},
|
||||
followers:{totalCount:0},
|
||||
following:{totalCount:0},
|
||||
issueComments:{totalCount:0},
|
||||
organizations:{totalCount:0},
|
||||
})
|
||||
},
|
||||
//Skip base content query and instantiate an empty user instance
|
||||
skip({login, data}) {
|
||||
data.user = {}
|
||||
for (const account of ["user", "organization"])
|
||||
postprocess?.[account]({login, data})
|
||||
data.account = "bypass"
|
||||
Object.assign(data.user, {
|
||||
databaseId:0,
|
||||
name:login,
|
||||
login,
|
||||
createdAt:new Date(),
|
||||
avatarUrl:`https://github.com/${login}.png`,
|
||||
websiteUrl:null,
|
||||
twitterUsername:login,
|
||||
repositories:{totalCount:0, totalDiskUsage:0, nodes:[]},
|
||||
packages:{totalCount:0},
|
||||
})
|
||||
},
|
||||
}
|
||||
calendar:{contributionCalendar:{weeks:[]}},
|
||||
repositoriesContributedTo:{totalCount:0},
|
||||
followers:{totalCount:0},
|
||||
following:{totalCount:0},
|
||||
issueComments:{totalCount:0},
|
||||
organizations:{totalCount:0},
|
||||
})
|
||||
},
|
||||
//Skip base content query and instantiate an empty user instance
|
||||
skip({login, data}) {
|
||||
data.user = {}
|
||||
for (const account of ["user", "organization"])
|
||||
postprocess?.[account]({login, data})
|
||||
data.account = "bypass"
|
||||
Object.assign(data.user, {
|
||||
databaseId:0,
|
||||
name:login,
|
||||
login,
|
||||
createdAt:new Date(),
|
||||
avatarUrl:`https://github.com/${login}.png`,
|
||||
websiteUrl:null,
|
||||
twitterUsername:login,
|
||||
repositories:{totalCount:0, totalDiskUsage:0, nodes:[]},
|
||||
packages:{totalCount:0},
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
//Legacy functions
|
||||
const legacy = {
|
||||
converter(value) {
|
||||
if (/^(?:[Tt]rue|[Oo]n|[Yy]es|1)$/.test(value))
|
||||
return true
|
||||
if (/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(value))
|
||||
return false
|
||||
if (Number.isFinite(Number(value)))
|
||||
return !!(Number(value))
|
||||
},
|
||||
}
|
||||
const legacy = {
|
||||
converter(value) {
|
||||
if (/^(?:[Tt]rue|[Oo]n|[Yy]es|1)$/.test(value))
|
||||
return true
|
||||
if (/^(?:[Ff]alse|[Oo]ff|[Nn]o|0)$/.test(value))
|
||||
return false
|
||||
if (Number.isFinite(Number(value)))
|
||||
return !!(Number(value))
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,77 +1,77 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, rest, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
export default async function({login, q, imports, data, rest, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.contributors))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {head, base, ignored, contributions} = imports.metadata.plugins.contributors.inputs({data, account, q})
|
||||
const repo = {owner:data.repo.owner.login, repo:data.repo.name}
|
||||
|
||||
//Retrieve head and base commits
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > querying api head and base commits`)
|
||||
const ref = {
|
||||
head:(await graphql(queries.contributors.commit({...repo, expression:head}))).repository.object,
|
||||
base:(await graphql(queries.contributors.commit({...repo, expression:base}))).repository.object,
|
||||
}
|
||||
|
||||
//Get commit activity
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > querying api for commits between [${ref.base?.abbreviatedOid ?? null}] and [${ref.head?.abbreviatedOid ?? null}]`)
|
||||
const commits = []
|
||||
for (let page = 1; ; page++) {
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > loading page ${page}`)
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.contributors))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {head, base, ignored, contributions} = imports.metadata.plugins.contributors.inputs({data, account, q})
|
||||
const repo = {owner:data.repo.owner.login, repo:data.repo.name}
|
||||
|
||||
//Retrieve head and base commits
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > querying api head and base commits`)
|
||||
const ref = {
|
||||
head:(await graphql(queries.contributors.commit({...repo, expression:head}))).repository.object,
|
||||
base:(await graphql(queries.contributors.commit({...repo, expression:base}))).repository.object,
|
||||
}
|
||||
|
||||
//Get commit activity
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > querying api for commits between [${ref.base?.abbreviatedOid ?? null}] and [${ref.head?.abbreviatedOid ?? null}]`)
|
||||
const commits = []
|
||||
for (let page = 1; ; page++) {
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > loading page ${page}`)
|
||||
try {
|
||||
const {data:loaded} = await rest.repos.listCommits({...repo, per_page:100, page})
|
||||
if (loaded.map(({sha}) => sha).includes(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)))
|
||||
break
|
||||
}
|
||||
if (!loaded.length) {
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > no more page to load`)
|
||||
break
|
||||
}
|
||||
commits.push(...loaded)
|
||||
}
|
||||
catch (error) {
|
||||
if (/Git Repository is empty/.test(error))
|
||||
break
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
//Remove commits after head
|
||||
const start = Math.max(0, commits.map(({sha}) => sha).indexOf(ref.head?.oid))
|
||||
commits.splice(0, start)
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > ${commits.length} commits loaded (${start} removed)`)
|
||||
|
||||
//Compute contributors and contributions
|
||||
let contributors = {}
|
||||
for (const {author:{login, avatar_url:avatar}, commit:{message = ""}} of commits) {
|
||||
if ((!login)||(ignored.includes(login))) {
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > ignored contributor "${login}"`)
|
||||
continue
|
||||
}
|
||||
if (!(login in contributors))
|
||||
contributors[login] = {avatar:await imports.imgb64(avatar), contributions:1, pr:[]}
|
||||
else {
|
||||
contributors[login].contributions++
|
||||
contributors[login].pr.push(...(message.match(/(?<=[(])#\d+(?=[)])/g) ?? []))
|
||||
}
|
||||
}
|
||||
contributors = Object.fromEntries(Object.entries(contributors).sort(([_an, a], [_bn, b]) => b.contributions - a.contributions))
|
||||
|
||||
//Filter pull requests
|
||||
for (const contributor of Object.values(contributors))
|
||||
contributor.pr = [...new Set(contributor.pr)]
|
||||
|
||||
//Results
|
||||
return {head, base, ref, list:contributors, contributions}
|
||||
const {data:loaded} = await rest.repos.listCommits({...repo, per_page:100, page})
|
||||
if (loaded.map(({sha}) => sha).includes(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)))
|
||||
break
|
||||
}
|
||||
if (!loaded.length) {
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > no more page to load`)
|
||||
break
|
||||
}
|
||||
commits.push(...loaded)
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
if (/Git Repository is empty/.test(error))
|
||||
break
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
//Remove commits after head
|
||||
const start = Math.max(0, commits.map(({sha}) => sha).indexOf(ref.head?.oid))
|
||||
commits.splice(0, start)
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > ${commits.length} commits loaded (${start} removed)`)
|
||||
|
||||
//Compute contributors and contributions
|
||||
let contributors = {}
|
||||
for (const {author:{login, avatar_url:avatar}, commit:{message = ""}} of commits) {
|
||||
if ((!login) || (ignored.includes(login))) {
|
||||
console.debug(`metrics/compute/${login}/plugins > contributors > ignored contributor "${login}"`)
|
||||
continue
|
||||
}
|
||||
if (!(login in contributors))
|
||||
contributors[login] = {avatar:await imports.imgb64(avatar), contributions:1, pr:[]}
|
||||
else {
|
||||
contributors[login].contributions++
|
||||
contributors[login].pr.push(...(message.match(/(?<=[(])#\d+(?=[)])/g) ?? []))
|
||||
}
|
||||
}
|
||||
contributors = Object.fromEntries(Object.entries(contributors).sort(([_an, a], [_bn, b]) => b.contributions - a.contributions))
|
||||
|
||||
//Filter pull requests
|
||||
for (const contributor of Object.values(contributors))
|
||||
contributor.pr = [...new Set(contributor.pr)]
|
||||
|
||||
//Results
|
||||
return {head, base, ref, list:contributors, contributions}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,134 +4,134 @@
|
||||
*/
|
||||
|
||||
//Setup
|
||||
export default async function({login, q}, {conf, data, rest, graphql, plugins, queries, account, convert, template}, {pending, imports}) {
|
||||
//Load inputs
|
||||
const {"config.animations":animations, "config.timezone":_timezone, "debug.flags":dflags} = imports.metadata.plugins.core.inputs({data, account, q})
|
||||
imports.metadata.templates[template].check({q, account, format:convert})
|
||||
export default async function({login, q}, {conf, data, rest, graphql, plugins, queries, account, convert, template}, {pending, imports}) {
|
||||
//Load inputs
|
||||
const {"config.animations":animations, "config.timezone":_timezone, "debug.flags":dflags} = imports.metadata.plugins.core.inputs({data, account, q})
|
||||
imports.metadata.templates[template].check({q, account, format:convert})
|
||||
|
||||
//Init
|
||||
const computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, 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}}
|
||||
const avatar = imports.imgb64(data.user.avatarUrl)
|
||||
data.computed = computed
|
||||
console.debug(`metrics/compute/${login} > formatting common metrics`)
|
||||
//Init
|
||||
const computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}}, 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}}
|
||||
const avatar = imports.imgb64(data.user.avatarUrl)
|
||||
data.computed = computed
|
||||
console.debug(`metrics/compute/${login} > formatting common metrics`)
|
||||
|
||||
//Timezone config
|
||||
if (_timezone) {
|
||||
const timezone = {name:_timezone, offset:0}
|
||||
data.config.timezone = timezone
|
||||
try {
|
||||
timezone.offset = Number(new Date().toLocaleString("fr", {timeZoneName:"short", timeZone:timezone.name}).match(/UTC[+](?<offset>\d+)/)?.groups?.offset*60*60*1000) || 0
|
||||
console.debug(`metrics/compute/${login} > timezone set to ${timezone.name} (${timezone.offset > 0 ? "+" : ""}${Math.round(timezone.offset/(60*60*1000))} hours)`)
|
||||
}
|
||||
catch {
|
||||
timezone.error = `Failed to use timezone "${timezone.name}"`
|
||||
console.debug(`metrics/compute/${login} > failed to use timezone "${timezone.name}"`)
|
||||
}
|
||||
}
|
||||
|
||||
//Animations
|
||||
data.animated = animations
|
||||
console.debug(`metrics/compute/${login} > animations ${data.animated ? "enabled" : "disabled"}`)
|
||||
|
||||
//Plugins
|
||||
for (const name of Object.keys(imports.plugins)) {
|
||||
if ((!plugins[name]?.enabled)||(!q[name]))
|
||||
continue
|
||||
pending.push((async() => {
|
||||
try {
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
|
||||
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries, account}, plugins[name])
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > completed`)
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > completed (error)`)
|
||||
data.plugins[name] = error
|
||||
}
|
||||
finally {
|
||||
const result = {name, result:data.plugins[name]}
|
||||
console.debug(imports.util.inspect(result, {depth:Infinity, maxStringLength:256}))
|
||||
return result
|
||||
}
|
||||
})())
|
||||
}
|
||||
|
||||
//Iterate through user's repositories
|
||||
for (const repository of data.user.repositories.nodes) {
|
||||
//Simple properties with totalCount
|
||||
for (const property of ["watchers", "stargazers", "issues_open", "issues_closed", "pr_open", "pr_closed", "pr_merged", "releases"])
|
||||
computed.repositories[property] += repository[property].totalCount
|
||||
//Forks
|
||||
computed.repositories.forks += repository.forkCount
|
||||
if (repository.isFork)
|
||||
computed.repositories.forked++
|
||||
//License
|
||||
if (repository.licenseInfo)
|
||||
computed.licenses.used[repository.licenseInfo.spdxId] = (computed.licenses.used[repository.licenseInfo.spdxId] ?? 0) + 1
|
||||
}
|
||||
|
||||
//Total disk usage
|
||||
computed.diskUsage = `${imports.bytes(data.user.repositories.totalDiskUsage*1000)}`
|
||||
|
||||
//Compute licenses stats
|
||||
computed.licenses.favorite = Object.entries(computed.licenses.used).sort(([_an, a], [_bn, b]) => b - a).slice(0, 1).map(([name, _value]) => name) ?? ""
|
||||
|
||||
//Compute total commits
|
||||
computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
|
||||
|
||||
//Compute registration date
|
||||
const diff = (Date.now()-(new Date(data.user.createdAt)).getTime())/(365*24*60*60*1000)
|
||||
const years = Math.floor(diff)
|
||||
const months = Math.floor((diff-years)*12)
|
||||
computed.registered = {years, months, diff}
|
||||
computed.registration = years ? `${years} year${imports.s(years)} ago` : months ? `${months} month${imports.s(months)} ago` : `${Math.ceil(diff*365)} day${imports.s(Math.ceil(diff*365))} ago`
|
||||
computed.cakeday = years > 1 ? [new Date(), new Date(data.user.createdAt)].map(date => date.toISOString().match(/(?<mmdd>\d{2}-\d{2})(?=T)/)?.groups?.mmdd).every((v, _, a) => v === a[0]) : false
|
||||
|
||||
//Compute calendar
|
||||
computed.calendar = data.user.calendar.contributionCalendar.weeks.flatMap(({contributionDays}) => contributionDays).slice(0, 14).reverse()
|
||||
|
||||
//Avatar (base64)
|
||||
computed.avatar = await avatar || "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||
|
||||
//Token scopes
|
||||
computed.token.scopes = conf.settings.notoken ? [] : (await rest.request("HEAD /")).headers["x-oauth-scopes"].split(", ")
|
||||
|
||||
//Meta
|
||||
data.meta = {version:conf.package.version, author:conf.package.author}
|
||||
|
||||
//Debug flags
|
||||
if (dflags.includes("--cakeday")) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --cakeday`)
|
||||
computed.cakeday = true
|
||||
}
|
||||
if (dflags.includes("--hireable")) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --hireable`)
|
||||
data.user.isHireable = true
|
||||
}
|
||||
if (dflags.includes("--halloween")) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --halloween`)
|
||||
//Haloween color replacer
|
||||
const halloween = content => content
|
||||
.replace(/--color-calendar-graph/g, "--color-calendar-halloween-graph")
|
||||
.replace(/#9be9a8/gi, "var(--color-calendar-halloween-graph-day-L1-bg)")
|
||||
.replace(/#40c463/gi, "var(--color-calendar-halloween-graph-day-L2-bg)")
|
||||
.replace(/#30a14e/gi, "var(--color-calendar-halloween-graph-day-L3-bg)")
|
||||
.replace(/#216e39/gi, "var(--color-calendar-halloween-graph-day-L4-bg)")
|
||||
//Update contribution calendar colors
|
||||
computed.calendar.map(day => day.color = halloween(day.color))
|
||||
//Update isocalendar colors
|
||||
const waiting = [...pending]
|
||||
pending.push((async() => {
|
||||
await Promise.all(waiting)
|
||||
if (data.plugins.isocalendar?.svg)
|
||||
data.plugins.isocalendar.svg = halloween(data.plugins.isocalendar.svg)
|
||||
return {name:"dflag.halloween", result:true}
|
||||
})())
|
||||
}
|
||||
if (dflags.includes("--error")) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --error`)
|
||||
throw new Error("Failed as requested by --error flag")
|
||||
}
|
||||
|
||||
//Results
|
||||
return null
|
||||
//Timezone config
|
||||
if (_timezone) {
|
||||
const timezone = {name:_timezone, offset:0}
|
||||
data.config.timezone = timezone
|
||||
try {
|
||||
timezone.offset = Number(new Date().toLocaleString("fr", {timeZoneName:"short", timeZone:timezone.name}).match(/UTC[+](?<offset>\d+)/)?.groups?.offset * 60 * 60 * 1000) || 0
|
||||
console.debug(`metrics/compute/${login} > timezone set to ${timezone.name} (${timezone.offset > 0 ? "+" : ""}${Math.round(timezone.offset / (60 * 60 * 1000))} hours)`)
|
||||
}
|
||||
catch {
|
||||
timezone.error = `Failed to use timezone "${timezone.name}"`
|
||||
console.debug(`metrics/compute/${login} > failed to use timezone "${timezone.name}"`)
|
||||
}
|
||||
}
|
||||
|
||||
//Animations
|
||||
data.animated = animations
|
||||
console.debug(`metrics/compute/${login} > animations ${data.animated ? "enabled" : "disabled"}`)
|
||||
|
||||
//Plugins
|
||||
for (const name of Object.keys(imports.plugins)) {
|
||||
if ((!plugins[name]?.enabled) || (!q[name]))
|
||||
continue
|
||||
pending.push((async () => {
|
||||
try {
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > started`)
|
||||
data.plugins[name] = await imports.plugins[name]({login, q, imports, data, computed, rest, graphql, queries, account}, plugins[name])
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > completed`)
|
||||
}
|
||||
catch (error) {
|
||||
console.debug(`metrics/compute/${login}/plugins > ${name} > completed (error)`)
|
||||
data.plugins[name] = error
|
||||
}
|
||||
finally {
|
||||
const result = {name, result:data.plugins[name]}
|
||||
console.debug(imports.util.inspect(result, {depth:Infinity, maxStringLength:256}))
|
||||
return result
|
||||
}
|
||||
})())
|
||||
}
|
||||
|
||||
//Iterate through user's repositories
|
||||
for (const repository of data.user.repositories.nodes) {
|
||||
//Simple properties with totalCount
|
||||
for (const property of ["watchers", "stargazers", "issues_open", "issues_closed", "pr_open", "pr_closed", "pr_merged", "releases"])
|
||||
computed.repositories[property] += repository[property].totalCount
|
||||
//Forks
|
||||
computed.repositories.forks += repository.forkCount
|
||||
if (repository.isFork)
|
||||
computed.repositories.forked++
|
||||
//License
|
||||
if (repository.licenseInfo)
|
||||
computed.licenses.used[repository.licenseInfo.spdxId] = (computed.licenses.used[repository.licenseInfo.spdxId] ?? 0) + 1
|
||||
}
|
||||
|
||||
//Total disk usage
|
||||
computed.diskUsage = `${imports.bytes(data.user.repositories.totalDiskUsage * 1000)}`
|
||||
|
||||
//Compute licenses stats
|
||||
computed.licenses.favorite = Object.entries(computed.licenses.used).sort(([_an, a], [_bn, b]) => b - a).slice(0, 1).map(([name, _value]) => name) ?? ""
|
||||
|
||||
//Compute total commits
|
||||
computed.commits += data.user.contributionsCollection.totalCommitContributions + data.user.contributionsCollection.restrictedContributionsCount
|
||||
|
||||
//Compute registration date
|
||||
const diff = (Date.now() - (new Date(data.user.createdAt)).getTime()) / (365 * 24 * 60 * 60 * 1000)
|
||||
const years = Math.floor(diff)
|
||||
const months = Math.floor((diff - years) * 12)
|
||||
computed.registered = {years, months, diff}
|
||||
computed.registration = years ? `${years} year${imports.s(years)} ago` : months ? `${months} month${imports.s(months)} ago` : `${Math.ceil(diff * 365)} day${imports.s(Math.ceil(diff * 365))} ago`
|
||||
computed.cakeday = years > 1 ? [new Date(), new Date(data.user.createdAt)].map(date => date.toISOString().match(/(?<mmdd>\d{2}-\d{2})(?=T)/)?.groups?.mmdd).every((v, _, a) => v === a[0]) : false
|
||||
|
||||
//Compute calendar
|
||||
computed.calendar = data.user.calendar.contributionCalendar.weeks.flatMap(({contributionDays}) => contributionDays).slice(0, 14).reverse()
|
||||
|
||||
//Avatar (base64)
|
||||
computed.avatar = await avatar || "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
|
||||
|
||||
//Token scopes
|
||||
computed.token.scopes = conf.settings.notoken ? [] : (await rest.request("HEAD /")).headers["x-oauth-scopes"].split(", ")
|
||||
|
||||
//Meta
|
||||
data.meta = {version:conf.package.version, author:conf.package.author}
|
||||
|
||||
//Debug flags
|
||||
if (dflags.includes("--cakeday")) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --cakeday`)
|
||||
computed.cakeday = true
|
||||
}
|
||||
if (dflags.includes("--hireable")) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --hireable`)
|
||||
data.user.isHireable = true
|
||||
}
|
||||
if (dflags.includes("--halloween")) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --halloween`)
|
||||
//Haloween color replacer
|
||||
const halloween = content => content
|
||||
.replace(/--color-calendar-graph/g, "--color-calendar-halloween-graph")
|
||||
.replace(/#9be9a8/gi, "var(--color-calendar-halloween-graph-day-L1-bg)")
|
||||
.replace(/#40c463/gi, "var(--color-calendar-halloween-graph-day-L2-bg)")
|
||||
.replace(/#30a14e/gi, "var(--color-calendar-halloween-graph-day-L3-bg)")
|
||||
.replace(/#216e39/gi, "var(--color-calendar-halloween-graph-day-L4-bg)")
|
||||
//Update contribution calendar colors
|
||||
computed.calendar.map(day => day.color = halloween(day.color))
|
||||
//Update isocalendar colors
|
||||
const waiting = [...pending]
|
||||
pending.push((async () => {
|
||||
await Promise.all(waiting)
|
||||
if (data.plugins.isocalendar?.svg)
|
||||
data.plugins.isocalendar.svg = halloween(data.plugins.isocalendar.svg)
|
||||
return {name:"dflag.halloween", result:true}
|
||||
})())
|
||||
}
|
||||
if (dflags.includes("--error")) {
|
||||
console.debug(`metrics/compute/${login} > applying dflag --error`)
|
||||
throw new Error("Failed as requested by --error flag")
|
||||
}
|
||||
|
||||
//Results
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
//Setup
|
||||
export default async function({login, data, computed, imports, q, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.followup))
|
||||
return null
|
||||
export default async function({login, data, computed, imports, q, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.followup))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {sections} = imports.metadata.plugins.followup.inputs({data, account, q})
|
||||
//Load inputs
|
||||
let {sections} = imports.metadata.plugins.followup.inputs({data, account, q})
|
||||
|
||||
//Define getters
|
||||
const followup = {
|
||||
sections,
|
||||
issues:{
|
||||
get count() {
|
||||
return this.open + this.closed
|
||||
},
|
||||
get open() {
|
||||
return computed.repositories.issues_open
|
||||
},
|
||||
get closed() {
|
||||
return computed.repositories.issues_closed
|
||||
},
|
||||
},
|
||||
pr:{
|
||||
get count() {
|
||||
return this.open + this.closed + this.merged
|
||||
},
|
||||
get open() {
|
||||
return computed.repositories.pr_open
|
||||
},
|
||||
get closed() {
|
||||
return computed.repositories.pr_closed
|
||||
},
|
||||
get merged() {
|
||||
return computed.repositories.pr_merged
|
||||
},
|
||||
},
|
||||
}
|
||||
//Define getters
|
||||
const followup = {
|
||||
sections,
|
||||
issues:{
|
||||
get count() {
|
||||
return this.open + this.closed
|
||||
},
|
||||
get open() {
|
||||
return computed.repositories.issues_open
|
||||
},
|
||||
get closed() {
|
||||
return computed.repositories.issues_closed
|
||||
},
|
||||
},
|
||||
pr:{
|
||||
get count() {
|
||||
return this.open + this.closed + this.merged
|
||||
},
|
||||
get open() {
|
||||
return computed.repositories.pr_open
|
||||
},
|
||||
get closed() {
|
||||
return computed.repositories.pr_closed
|
||||
},
|
||||
get merged() {
|
||||
return computed.repositories.pr_merged
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
//Load user issues and pull requests
|
||||
if (sections.includes("user")) {
|
||||
const {user} = await graphql(queries.followup.user({login}))
|
||||
followup.user = {
|
||||
issues:{
|
||||
get count() {
|
||||
return this.open + this.closed
|
||||
},
|
||||
open:user.issues_open.totalCount,
|
||||
closed:user.issues_closed.totalCount,
|
||||
},
|
||||
pr:{
|
||||
get count() {
|
||||
return this.open + this.closed + this.merged
|
||||
},
|
||||
open:user.pr_open.totalCount,
|
||||
closed:user.pr_closed.totalCount,
|
||||
merged:user.pr_merged.totalCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//Results
|
||||
return followup
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
//Load user issues and pull requests
|
||||
if (sections.includes("user")) {
|
||||
const {user} = await graphql(queries.followup.user({login}))
|
||||
followup.user = {
|
||||
issues:{
|
||||
get count() {
|
||||
return this.open + this.closed
|
||||
},
|
||||
open:user.issues_open.totalCount,
|
||||
closed:user.issues_closed.totalCount,
|
||||
},
|
||||
pr:{
|
||||
get count() {
|
||||
return this.open + this.closed + this.merged
|
||||
},
|
||||
open:user.pr_open.totalCount,
|
||||
closed:user.pr_closed.totalCount,
|
||||
merged:user.pr_merged.totalCount,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
//Results
|
||||
return followup
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,52 +1,52 @@
|
||||
//Setup
|
||||
export default async function({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.gists))
|
||||
return null
|
||||
export default async function({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.gists))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
imports.metadata.plugins.gists.inputs({data, account, q})
|
||||
//Load inputs
|
||||
imports.metadata.plugins.gists.inputs({data, account, q})
|
||||
|
||||
//Query gists from GitHub API
|
||||
const gists = []
|
||||
{
|
||||
//Iterate through gists
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
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}"` : ""}))
|
||||
cursor = edges?.[edges?.length-1]?.cursor
|
||||
gists.push(...nodes)
|
||||
gists.totalCount = totalCount
|
||||
pushed = nodes.length
|
||||
} while ((pushed)&&(cursor))
|
||||
console.debug(`metrics/compute/${login}/plugins > gists > loaded ${gists.length} gists`)
|
||||
}
|
||||
//Query gists from GitHub API
|
||||
const gists = []
|
||||
{
|
||||
//Iterate through gists
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
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}"` : ""}))
|
||||
cursor = edges?.[edges?.length - 1]?.cursor
|
||||
gists.push(...nodes)
|
||||
gists.totalCount = totalCount
|
||||
pushed = nodes.length
|
||||
} while ((pushed) && (cursor))
|
||||
console.debug(`metrics/compute/${login}/plugins > gists > loaded ${gists.length} gists`)
|
||||
}
|
||||
|
||||
//Iterate through gists
|
||||
console.debug(`metrics/compute/${login}/plugins > gists > processing ${gists.length} gists`)
|
||||
let comments = 0, files = 0, forks = 0, stargazers = 0
|
||||
for (const gist of gists) {
|
||||
//Skip forks
|
||||
if (gist.isFork)
|
||||
continue
|
||||
//Compute stars, forks, comments and files count
|
||||
stargazers += gist.stargazerCount
|
||||
forks += gist.forks.totalCount
|
||||
comments += gist.comments.totalCount
|
||||
files += gist.files.length
|
||||
}
|
||||
//Iterate through gists
|
||||
console.debug(`metrics/compute/${login}/plugins > gists > processing ${gists.length} gists`)
|
||||
let comments = 0, files = 0, forks = 0, stargazers = 0
|
||||
for (const gist of gists) {
|
||||
//Skip forks
|
||||
if (gist.isFork)
|
||||
continue
|
||||
//Compute stars, forks, comments and files count
|
||||
stargazers += gist.stargazerCount
|
||||
forks += gist.forks.totalCount
|
||||
comments += gist.comments.totalCount
|
||||
files += gist.files.length
|
||||
}
|
||||
|
||||
//Results
|
||||
return {totalCount:gists.totalCount, stargazers, forks, files, comments}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {totalCount:gists.totalCount, stargazers, forks, files, comments}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +1,127 @@
|
||||
//Setup
|
||||
export default async function({login, data, rest, imports, q, account}, {enabled = false, ...defaults} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.habits))
|
||||
return null
|
||||
export default async function({login, data, rest, imports, q, account}, {enabled = false, ...defaults} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.habits))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {from, days, facts, charts} = imports.metadata.plugins.habits.inputs({data, account, q}, defaults)
|
||||
//Load inputs
|
||||
let {from, days, facts, charts} = imports.metadata.plugins.habits.inputs({data, account, q}, defaults)
|
||||
|
||||
//Initialization
|
||||
const habits = {facts, charts, commits:{hour:NaN, hours:{}, day:NaN, days:{}}, indents:{style:"", spaces:0, tabs:0}, linguist:{available:false, ordered:[], languages:{}}}
|
||||
const pages = Math.ceil(from/100)
|
||||
const offset = data.config.timezone?.offset ?? 0
|
||||
//Initialization
|
||||
const habits = {facts, charts, commits:{hour:NaN, hours:{}, day:NaN, days:{}}, indents:{style:"", spaces:0, tabs:0}, linguist:{available:false, ordered:[], languages:{}}}
|
||||
const pages = Math.ceil(from / 100)
|
||||
const offset = data.config.timezone?.offset ?? 0
|
||||
|
||||
//Get user recent activity
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > querying api`)
|
||||
const events = []
|
||||
try {
|
||||
for (let page = 1; page < pages; page++) {
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > loading page ${page}`)
|
||||
events.push(...(await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > no more page to load`)
|
||||
}
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > ${events.length} events loaded`)
|
||||
|
||||
//Get user recent commits
|
||||
const commits = events
|
||||
.filter(({type}) => type === "PushEvent")
|
||||
.filter(({actor}) => account === "organization" ? true : actor.login === login)
|
||||
.filter(({created_at}) => new Date(created_at) > new Date(Date.now()-days*24*60*60*1000))
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > filtered out ${commits.length} push events over last ${days} days`)
|
||||
|
||||
//Retrieve edited files and filter edited lines (those starting with +/-) from patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > loading patches`)
|
||||
const patches = [...await Promise.allSettled(commits
|
||||
.flatMap(({payload}) => payload.commits).map(commit => commit.url)
|
||||
.map(async commit => (await rest.request(commit)).data.files))]
|
||||
.filter(({status}) => status === "fulfilled")
|
||||
.map(({value}) => value)
|
||||
.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")}))
|
||||
|
||||
//Commit day
|
||||
{
|
||||
//Compute commit days
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching most active day of week`)
|
||||
const days = commits.map(({created_at}) => (new Date(new Date(created_at).getTime() + offset)).getDay())
|
||||
for (const day of days)
|
||||
habits.commits.days[day] = (habits.commits.days[day] ?? 0) + 1
|
||||
habits.commits.days.max = Math.max(...Object.values(habits.commits.days))
|
||||
//Compute day with most commits
|
||||
habits.commits.day = days.length ? ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][Object.entries(habits.commits.days).sort(([_an, a], [_bn, b]) => b - a).map(([day, _occurence]) => day)[0]] ?? NaN : NaN
|
||||
}
|
||||
|
||||
//Commit hour
|
||||
{
|
||||
//Compute commit hours
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching most active time of day`)
|
||||
const hours = commits.map(({created_at}) => (new Date(new Date(created_at).getTime() + offset)).getHours())
|
||||
for (const hour of hours)
|
||||
habits.commits.hours[hour] = (habits.commits.hours[hour] ?? 0) + 1
|
||||
habits.commits.hours.max = Math.max(...Object.values(habits.commits.hours))
|
||||
//Compute hour with most commits
|
||||
habits.commits.hour = hours.length ? `${Object.entries(habits.commits.hours).sort(([_an, a], [_bn, b]) => b - a).map(([hour, _occurence]) => hour)[0]}`.padStart(2, "0") : NaN
|
||||
}
|
||||
|
||||
//Indent style
|
||||
{
|
||||
//Attempt to guess whether tabs or spaces are used in patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching indent style`)
|
||||
patches
|
||||
.map(({patch}) => patch.match(/((?:\t)|(?:[ ]{2})) /gm) ?? [])
|
||||
.forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++)
|
||||
habits.indents.style = habits.indents.spaces > habits.indents.tabs ? "spaces" : habits.indents.tabs > habits.indents.spaces ? "tabs" : ""
|
||||
}
|
||||
|
||||
//Linguist
|
||||
if (charts) {
|
||||
//Check if linguist exists
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching recently used languages using linguist`)
|
||||
if ((patches.length)&&(await imports.which("github-linguist"))) {
|
||||
//Setup for linguist
|
||||
habits.linguist.available = true
|
||||
const path = imports.paths.join(imports.os.tmpdir(), `${commits[0]?.actor?.id ?? 0}`)
|
||||
//Create temporary directory and save patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > creating temp dir ${path} with ${patches.length} files`)
|
||||
await imports.fs.mkdir(path, {recursive:true})
|
||||
await Promise.all(patches.map(({name, patch}, i) => imports.fs.writeFile(imports.paths.join(path, `${i}${imports.paths.extname(name)}`), patch)))
|
||||
//Create temporary git repository
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > creating temp git repository`)
|
||||
const git = await imports.git(path)
|
||||
await git.init().add(".").addConfig("user.name", "linguist").addConfig("user.email", "<>").commit("linguist").status()
|
||||
//Spawn linguist process
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > running linguist`)
|
||||
;(await imports.run("github-linguist --breakdown", {cwd:path}))
|
||||
//Parse linguist result
|
||||
.split("\n").map(line => line.match(/(?<value>[\d.]+)%\s+(?<language>[\s\S]+)$/)?.groups).filter(line => line)
|
||||
.map(({value, language}) => habits.linguist.languages[language] = (habits.linguist.languages[language] ?? 0) + value/100)
|
||||
habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([_an, a], [_bn, b]) => b - a)
|
||||
//Cleaning
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > cleaning temp dir ${path}`)
|
||||
await imports.fs.rmdir(path, {recursive:true})
|
||||
}
|
||||
else
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > linguist not available`)
|
||||
}
|
||||
|
||||
//Results
|
||||
return habits
|
||||
//Get user recent activity
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > querying api`)
|
||||
const events = []
|
||||
try {
|
||||
for (let page = 1; page < pages; page++) {
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > loading page ${page}`)
|
||||
events.push(...(await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data)
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
catch {
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > no more page to load`)
|
||||
}
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > ${events.length} events loaded`)
|
||||
|
||||
//Get user recent commits
|
||||
const commits = events
|
||||
.filter(({type}) => type === "PushEvent")
|
||||
.filter(({actor}) => account === "organization" ? true : actor.login === login)
|
||||
.filter(({created_at}) => new Date(created_at) > new Date(Date.now() - days * 24 * 60 * 60 * 1000))
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > filtered out ${commits.length} push events over last ${days} days`)
|
||||
|
||||
//Retrieve edited files and filter edited lines (those starting with +/-) from patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > loading patches`)
|
||||
const patches = [
|
||||
...await Promise.allSettled(
|
||||
commits
|
||||
.flatMap(({payload}) => payload.commits).map(commit => commit.url)
|
||||
.map(async commit => (await rest.request(commit)).data.files),
|
||||
),
|
||||
]
|
||||
.filter(({status}) => status === "fulfilled")
|
||||
.map(({value}) => value)
|
||||
.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")}))
|
||||
|
||||
//Commit day
|
||||
{
|
||||
//Compute commit days
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching most active day of week`)
|
||||
const days = commits.map(({created_at}) => (new Date(new Date(created_at).getTime() + offset)).getDay())
|
||||
for (const day of days)
|
||||
habits.commits.days[day] = (habits.commits.days[day] ?? 0) + 1
|
||||
habits.commits.days.max = Math.max(...Object.values(habits.commits.days))
|
||||
//Compute day with most commits
|
||||
habits.commits.day = days.length ? ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][Object.entries(habits.commits.days).sort(([_an, a], [_bn, b]) => b - a).map(([day, _occurence]) => day)[0]] ?? NaN : NaN
|
||||
}
|
||||
|
||||
//Commit hour
|
||||
{
|
||||
//Compute commit hours
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching most active time of day`)
|
||||
const hours = commits.map(({created_at}) => (new Date(new Date(created_at).getTime() + offset)).getHours())
|
||||
for (const hour of hours)
|
||||
habits.commits.hours[hour] = (habits.commits.hours[hour] ?? 0) + 1
|
||||
habits.commits.hours.max = Math.max(...Object.values(habits.commits.hours))
|
||||
//Compute hour with most commits
|
||||
habits.commits.hour = hours.length ? `${Object.entries(habits.commits.hours).sort(([_an, a], [_bn, b]) => b - a).map(([hour, _occurence]) => hour)[0]}`.padStart(2, "0") : NaN
|
||||
}
|
||||
|
||||
//Indent style
|
||||
{
|
||||
//Attempt to guess whether tabs or spaces are used in patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching indent style`)
|
||||
patches
|
||||
.map(({patch}) => patch.match(/((?:\t)|(?:[ ]{2})) /gm) ?? [])
|
||||
.forEach(indent => habits.indents[/^\t/.test(indent) ? "tabs" : "spaces"]++)
|
||||
habits.indents.style = habits.indents.spaces > habits.indents.tabs ? "spaces" : habits.indents.tabs > habits.indents.spaces ? "tabs" : ""
|
||||
}
|
||||
|
||||
//Linguist
|
||||
if (charts) {
|
||||
//Check if linguist exists
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > searching recently used languages using linguist`)
|
||||
if ((patches.length) && (await imports.which("github-linguist"))) {
|
||||
//Setup for linguist
|
||||
habits.linguist.available = true
|
||||
const path = imports.paths.join(imports.os.tmpdir(), `${commits[0]?.actor?.id ?? 0}`)
|
||||
//Create temporary directory and save patches
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > creating temp dir ${path} with ${patches.length} files`)
|
||||
await imports.fs.mkdir(path, {recursive:true})
|
||||
await Promise.all(patches.map(({name, patch}, i) => imports.fs.writeFile(imports.paths.join(path, `${i}${imports.paths.extname(name)}`), patch)))
|
||||
//Create temporary git repository
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > creating temp git repository`)
|
||||
const git = await imports.git(path)
|
||||
await git.init().add(".").addConfig("user.name", "linguist").addConfig("user.email", "<>").commit("linguist").status()
|
||||
//Spawn linguist process
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > running linguist`)
|
||||
;(await imports.run("github-linguist --breakdown", {cwd:path}))
|
||||
//Parse linguist result
|
||||
.split("\n").map(line => line.match(/(?<value>[\d.]+)%\s+(?<language>[\s\S]+)$/)?.groups).filter(line => line)
|
||||
.map(({value, language}) => habits.linguist.languages[language] = (habits.linguist.languages[language] ?? 0) + value / 100)
|
||||
habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([_an, a], [_bn, b]) => b - a)
|
||||
//Cleaning
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > cleaning temp dir ${path}`)
|
||||
await imports.fs.rmdir(path, {recursive:true})
|
||||
}
|
||||
else
|
||||
console.debug(`metrics/compute/${login}/plugins > habits > linguist not available`)
|
||||
|
||||
}
|
||||
|
||||
//Results
|
||||
return habits
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,31 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.introduction))
|
||||
return null
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.introduction))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {title} = imports.metadata.plugins.introduction.inputs({data, account, q})
|
||||
//Load inputs
|
||||
let {title} = imports.metadata.plugins.introduction.inputs({data, account, q})
|
||||
|
||||
//Context
|
||||
let context = {mode:account, login}
|
||||
if (q.repo) {
|
||||
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()
|
||||
context = {...context, mode:"repository", owner, repo}
|
||||
}
|
||||
//Context
|
||||
let context = {mode:account, login}
|
||||
if (q.repo) {
|
||||
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()
|
||||
context = {...context, mode:"repository", owner, repo}
|
||||
}
|
||||
|
||||
//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]]
|
||||
//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]]
|
||||
|
||||
//Results
|
||||
return {mode:context.mode, title, text}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {mode:context.mode, title, text}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,99 @@
|
||||
//Setup
|
||||
export default async function({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.isocalendar))
|
||||
return null
|
||||
export default async function({login, data, graphql, q, imports, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.isocalendar))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {duration} = imports.metadata.plugins.isocalendar.inputs({data, account, q})
|
||||
//Load inputs
|
||||
let {duration} = imports.metadata.plugins.isocalendar.inputs({data, account, q})
|
||||
|
||||
//Compute start day
|
||||
const now = new Date()
|
||||
const start = new Date(now)
|
||||
if (duration === "full-year")
|
||||
start.setFullYear(now.getFullYear()-1)
|
||||
else
|
||||
start.setHours(-24*180)
|
||||
//Compute start day
|
||||
const now = new Date()
|
||||
const start = new Date(now)
|
||||
if (duration === "full-year")
|
||||
start.setFullYear(now.getFullYear() - 1)
|
||||
else
|
||||
start.setHours(-24 * 180)
|
||||
|
||||
//Compute padding to ensure last row is complete
|
||||
const padding = new Date(start)
|
||||
padding.setHours(-14*24)
|
||||
//Compute padding to ensure last row is complete
|
||||
const padding = new Date(start)
|
||||
padding.setHours(-14 * 24)
|
||||
|
||||
//Retrieve contribution calendar from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > querying api`)
|
||||
const calendar = {}
|
||||
for (const [name, from, to] of [["padding", padding, start], ["weeks", start, now]]) {
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > loading ${name} from "${from.toISOString()}" to "${to.toISOString()}"`)
|
||||
const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(queries.isocalendar.calendar({login, from:from.toISOString(), to:to.toISOString()}))
|
||||
calendar[name] = weeks
|
||||
}
|
||||
//Retrieve contribution calendar from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > querying api`)
|
||||
const calendar = {}
|
||||
for (const [name, from, to] of [["padding", padding, start], ["weeks", start, now]]) {
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > loading ${name} from "${from.toISOString()}" to "${to.toISOString()}"`)
|
||||
const {user:{calendar:{contributionCalendar:{weeks}}}} = await graphql(queries.isocalendar.calendar({login, from:from.toISOString(), to:to.toISOString()}))
|
||||
calendar[name] = weeks
|
||||
}
|
||||
|
||||
//Apply padding
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > applying padding`)
|
||||
const firstweek = calendar.weeks[0].contributionDays
|
||||
const padded = calendar.padding.flatMap(({contributionDays}) => contributionDays).filter(({date}) => !firstweek.map(({date}) => date).includes(date))
|
||||
while (firstweek.length < 7)
|
||||
firstweek.unshift(padded.pop())
|
||||
//Apply padding
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > applying padding`)
|
||||
const firstweek = calendar.weeks[0].contributionDays
|
||||
const padded = calendar.padding.flatMap(({contributionDays}) => contributionDays).filter(({date}) => !firstweek.map(({date}) => date).includes(date))
|
||||
while (firstweek.length < 7)
|
||||
firstweek.unshift(padded.pop())
|
||||
|
||||
//Compute the highest contributions in a day, streaks and average commits per day
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing stats`)
|
||||
let average = 0, max = 0, streak = {max:0, current:0}, values = []
|
||||
for (const week of calendar.weeks) {
|
||||
for (const day of week.contributionDays) {
|
||||
values.push(day.contributionCount)
|
||||
max = Math.max(max, day.contributionCount)
|
||||
streak.current = day.contributionCount ? streak.current+1 : 0
|
||||
streak.max = Math.max(streak.max, streak.current)
|
||||
}
|
||||
}
|
||||
average = (values.reduce((a, b) => a + b, 0)/values.length).toFixed(2).replace(/[.]0+$/, "")
|
||||
//Compute the highest contributions in a day, streaks and average commits per day
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing stats`)
|
||||
let average = 0, max = 0, streak = {max:0, current:0}, values = []
|
||||
for (const week of calendar.weeks) {
|
||||
for (const day of week.contributionDays) {
|
||||
values.push(day.contributionCount)
|
||||
max = Math.max(max, day.contributionCount)
|
||||
streak.current = day.contributionCount ? streak.current + 1 : 0
|
||||
streak.max = Math.max(streak.max, streak.current)
|
||||
}
|
||||
}
|
||||
average = (values.reduce((a, b) => a + b, 0) / values.length).toFixed(2).replace(/[.]0+$/, "")
|
||||
|
||||
//Compute SVG
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing svg render`)
|
||||
const size = 6
|
||||
let i = 0, j = 0
|
||||
let svg = `
|
||||
//Compute SVG
|
||||
console.debug(`metrics/compute/${login}/plugins > isocalendar > computing svg render`)
|
||||
const size = 6
|
||||
let i = 0, j = 0
|
||||
let svg = `
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" style="margin-top: -52px;" viewBox="0,0 480,${duration === "full-year" ? 270 : 170}">
|
||||
${[1, 2].map(k => `
|
||||
${
|
||||
[1, 2].map(k => `
|
||||
<filter id="brightness${k}">
|
||||
<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>
|
||||
</filter>`)
|
||||
.join("")}
|
||||
</filter>`
|
||||
)
|
||||
.join("")
|
||||
}
|
||||
<g transform="scale(4) translate(12, 0)">`
|
||||
//Iterate through weeks
|
||||
for (const week of calendar.weeks) {
|
||||
svg += `<g transform="translate(${i*1.7}, ${i})">`
|
||||
j = 0
|
||||
//Iterate through days
|
||||
for (const day of week.contributionDays) {
|
||||
const ratio = day.contributionCount/max
|
||||
svg += `
|
||||
<g transform="translate(${j*-1.7}, ${j+(1-ratio)*size})">
|
||||
//Iterate through weeks
|
||||
for (const week of calendar.weeks) {
|
||||
svg += `<g transform="translate(${i * 1.7}, ${i})">`
|
||||
j = 0
|
||||
//Iterate through days
|
||||
for (const day of week.contributionDays) {
|
||||
const ratio = day.contributionCount / max
|
||||
svg += `
|
||||
<g transform="translate(${j * -1.7}, ${j + (1 - ratio) * size})">
|
||||
<path fill="${day.color}" d="M1.7,2 0,1 1.7,0 3.4,1 z" />
|
||||
<path fill="${day.color}" filter="url(#brightness1)" d="M0,1 1.7,2 1.7,${2+ratio*size} 0,${1+ratio*size} z" />
|
||||
<path fill="${day.color}" filter="url(#brightness2)" d="M1.7,2 3.4,1 3.4,${1+ratio*size} 1.7,${2+ratio*size} z" />
|
||||
<path fill="${day.color}" filter="url(#brightness1)" d="M0,1 1.7,2 1.7,${2 + ratio * size} 0,${1 + ratio * size} z" />
|
||||
<path fill="${day.color}" filter="url(#brightness2)" d="M1.7,2 3.4,1 3.4,${1 + ratio * size} 1.7,${2 + ratio * size} z" />
|
||||
</g>`
|
||||
j++
|
||||
}
|
||||
svg += "</g>"
|
||||
i++
|
||||
}
|
||||
svg += "</g></svg>"
|
||||
j++
|
||||
}
|
||||
svg += "</g>"
|
||||
i++
|
||||
}
|
||||
svg += "</g></svg>"
|
||||
|
||||
//Results
|
||||
return {streak, max, average, svg, duration}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {streak, max, average, svg, duration}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,64 +1,64 @@
|
||||
//Setup
|
||||
export default async function({login, data, imports, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.languages))
|
||||
return null
|
||||
export default async function({login, data, imports, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.languages))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {ignored, skipped, colors, details, threshold, limit} = imports.metadata.plugins.languages.inputs({data, account, q})
|
||||
threshold = (Number(threshold.replace(/%$/, ""))||0)/100
|
||||
skipped.push(...data.shared["repositories.skipped"])
|
||||
if (!limit)
|
||||
limit = Infinity
|
||||
//Load inputs
|
||||
let {ignored, skipped, colors, details, threshold, limit} = imports.metadata.plugins.languages.inputs({data, account, q})
|
||||
threshold = (Number(threshold.replace(/%$/, "")) || 0) / 100
|
||||
skipped.push(...data.shared["repositories.skipped"])
|
||||
if (!limit)
|
||||
limit = Infinity
|
||||
|
||||
//Custom colors
|
||||
const colorsets = JSON.parse(`${await imports.fs.readFile(`${imports.__module(import.meta.url)}/colorsets.json`)}`)
|
||||
if ((`${colors}` in colorsets)&&(limit <= 8))
|
||||
colors = colorsets[`${colors}`]
|
||||
colors = Object.fromEntries(decodeURIComponent(colors).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x).map(x => x.split(":").map(x => x.trim())))
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > custom colors ${JSON.stringify(colors)}`)
|
||||
//Custom colors
|
||||
const colorsets = JSON.parse(`${await imports.fs.readFile(`${imports.__module(import.meta.url)}/colorsets.json`)}`)
|
||||
if ((`${colors}` in colorsets) && (limit <= 8))
|
||||
colors = colorsets[`${colors}`]
|
||||
colors = Object.fromEntries(decodeURIComponent(colors).split(",").map(x => x.trim().toLocaleLowerCase()).filter(x => x).map(x => x.split(":").map(x => x.trim())))
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > custom colors ${JSON.stringify(colors)}`)
|
||||
|
||||
//Iterate through user's repositories and retrieve languages data
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > processing ${data.user.repositories.nodes.length} repositories`)
|
||||
const languages = {details, colors:{}, total:0, stats:{}}
|
||||
for (const repository of data.user.repositories.nodes) {
|
||||
//Skip repository if asked
|
||||
if ((skipped.includes(repository.name.toLocaleLowerCase()))||(skipped.includes(`${repository.owner.login}/${repository.name}`.toLocaleLowerCase()))) {
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > skipped repository ${repository.owner.login}/${repository.name}`)
|
||||
continue
|
||||
}
|
||||
//Process repository languages
|
||||
for (const {size, node:{color, name}} of Object.values(repository.languages.edges)) {
|
||||
//Ignore language if asked
|
||||
if (ignored.includes(name.toLocaleLowerCase())) {
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > ignored language ${name}`)
|
||||
continue
|
||||
}
|
||||
//Update language stats
|
||||
languages.stats[name] = (languages.stats[name] ?? 0) + size
|
||||
languages.colors[name] = colors[name.toLocaleLowerCase()] ?? color ?? "#ededed"
|
||||
languages.total += size
|
||||
}
|
||||
}
|
||||
|
||||
//Compute languages stats
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > computing stats`)
|
||||
languages.favorites = Object.entries(languages.stats).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/languages.total > threshold)
|
||||
const visible = {total:Object.values(languages.favorites).map(({size}) => size).reduce((a, b) => a + b, 0)}
|
||||
for (let i = 0; i < languages.favorites.length; i++) {
|
||||
languages.favorites[i].value /= visible.total
|
||||
languages.favorites[i].x = (languages.favorites[i-1]?.x ?? 0) + (languages.favorites[i-1]?.value ?? 0)
|
||||
if ((colors[i])&&(!colors[languages.favorites[i].name.toLocaleLowerCase()]))
|
||||
languages.favorites[i].color = colors[i]
|
||||
}
|
||||
|
||||
//Results
|
||||
return languages
|
||||
//Iterate through user's repositories and retrieve languages data
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > processing ${data.user.repositories.nodes.length} repositories`)
|
||||
const languages = {details, colors:{}, total:0, stats:{}}
|
||||
for (const repository of data.user.repositories.nodes) {
|
||||
//Skip repository if asked
|
||||
if ((skipped.includes(repository.name.toLocaleLowerCase())) || (skipped.includes(`${repository.owner.login}/${repository.name}`.toLocaleLowerCase()))) {
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > skipped repository ${repository.owner.login}/${repository.name}`)
|
||||
continue
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
//Process repository languages
|
||||
for (const {size, node:{color, name}} of Object.values(repository.languages.edges)) {
|
||||
//Ignore language if asked
|
||||
if (ignored.includes(name.toLocaleLowerCase())) {
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > ignored language ${name}`)
|
||||
continue
|
||||
}
|
||||
//Update language stats
|
||||
languages.stats[name] = (languages.stats[name] ?? 0) + size
|
||||
languages.colors[name] = colors[name.toLocaleLowerCase()] ?? color ?? "#ededed"
|
||||
languages.total += size
|
||||
}
|
||||
}
|
||||
|
||||
//Compute languages stats
|
||||
console.debug(`metrics/compute/${login}/plugins > languages > computing stats`)
|
||||
languages.favorites = Object.entries(languages.stats).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 / languages.total > threshold)
|
||||
const visible = {total:Object.values(languages.favorites).map(({size}) => size).reduce((a, b) => a + b, 0)}
|
||||
for (let i = 0; i < languages.favorites.length; i++) {
|
||||
languages.favorites[i].value /= visible.total
|
||||
languages.favorites[i].x = (languages.favorites[i - 1]?.x ?? 0) + (languages.favorites[i - 1]?.value ?? 0)
|
||||
if ((colors[i]) && (!colors[languages.favorites[i].name.toLocaleLowerCase()]))
|
||||
languages.favorites[i].color = colors[i]
|
||||
}
|
||||
|
||||
//Results
|
||||
return languages
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,149 +1,160 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.licenses))
|
||||
return null
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.licenses))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {setup, ratio, legal} = imports.metadata.plugins.licenses.inputs({data, account, q})
|
||||
//Load inputs
|
||||
let {setup, ratio, legal} = imports.metadata.plugins.licenses.inputs({data, account, q})
|
||||
|
||||
//Initialization
|
||||
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 {used, text} = result
|
||||
//Initialization
|
||||
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 {used, text} = result
|
||||
|
||||
//Register existing licenses properties
|
||||
const licenses = Object.fromEntries((await graphql(queries.licenses())).licenses.map(license => [license.key, license]))
|
||||
for (const license of Object.values(licenses)) {
|
||||
//dprint-ignore
|
||||
[...license.limitations, ...license.conditions, ...license.permissions].flat().map(({key, label}) => text[key] = label)
|
||||
}
|
||||
colors(licenses)
|
||||
//Register existing licenses properties
|
||||
const licenses = Object.fromEntries((await graphql(queries.licenses())).licenses.map(license => [license.key, license]))
|
||||
for (const license of Object.values(licenses)) {
|
||||
//dprint-ignore
|
||||
[...license.limitations, ...license.conditions, ...license.permissions].flat().map(({key, label}) => text[key] = label)
|
||||
}
|
||||
colors(licenses)
|
||||
|
||||
//Check if licensed exists
|
||||
if (await imports.which("licensed")) {
|
||||
//Setup for licensed
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > searching dependencies licenses using licensed`)
|
||||
const path = imports.paths.join(imports.os.tmpdir(), `${repository.databaseId}`)
|
||||
//Create temporary directory
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > creating temp dir ${path}`)
|
||||
await imports.fs.rmdir(path, {recursive:true})
|
||||
await imports.fs.mkdir(path, {recursive:true})
|
||||
//Clone repository
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > cloning temp git repository ${repository.url} to ${path}`)
|
||||
const git = imports.git(path)
|
||||
await git.clone(repository.url, path)
|
||||
//Run setup
|
||||
if (setup) {
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > running setup [${setup}]`)
|
||||
await imports.run(setup, {cwd:path}, {prefixed:false})
|
||||
}
|
||||
//Create configuration file if needed
|
||||
if (!(await imports.fs.stat(imports.paths.join(path, ".licensed.yml")).then(() => 1).catch(() => 0))) {
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > building .licensed.yml configuration file`)
|
||||
await imports.fs.writeFile(imports.paths.join(path, ".licensed.yml"), [
|
||||
"cache_path: .licensed",
|
||||
].join("\n"))
|
||||
}
|
||||
else
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > a .licensed.yml configuration file already exists`)
|
||||
//Spawn licensed process
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > running licensed`)
|
||||
JSON.parse(await imports.run("licensed list --format=json --licenses", {cwd:path})).apps
|
||||
.map(({sources}) => sources?.flatMap(source => source.dependencies.map(({dependency, license}) => {
|
||||
used[license] = (used[license] ?? 0) + 1
|
||||
result.dependencies.push(dependency)
|
||||
result.known += (license in licenses)
|
||||
result.unknown += !(license in licenses)
|
||||
})))
|
||||
//Cleaning
|
||||
console.debug(`metrics/compute/${login}/plugins > licensed > cleaning temp dir ${path}`)
|
||||
await imports.fs.rmdir(path, {recursive:true})
|
||||
}
|
||||
else
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > licensed not available`)
|
||||
|
||||
//List licenses properties
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > compute licenses properties`)
|
||||
const base = {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}))
|
||||
for (const properties of Object.keys(base)) {
|
||||
//Base license
|
||||
if (repository.licenseInfo)
|
||||
licenses[repository.licenseInfo.key]?.[properties]?.map(({key}) => base[properties].add(key))
|
||||
//Combined licenses
|
||||
for (const {key} of detected)
|
||||
licenses[key]?.[properties]?.map(({key}) => combined[properties].add(key))
|
||||
}
|
||||
|
||||
//Merge limitations and 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()
|
||||
//Remove base permissions conflicting with inherited limitations
|
||||
result.permissions = [...base.permissions].filter(key => !combined.limitations.has(key)).map(key => ({key, text:text[key]}))
|
||||
|
||||
//Count used licenses
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > computing ratio`)
|
||||
const total = Object.values(used).reduce((a, b) => a + b, 0)
|
||||
//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((a, b) => a.order === b.order ? b.count - a.count : b.order - a.order)
|
||||
for (let i = 0; i < list.length; i++)
|
||||
list[i].x = (list[i-1]?.x ?? 0) + (list[i-1]?.value ?? 0)
|
||||
//Save ratios
|
||||
result.list = list
|
||||
|
||||
//Results
|
||||
return result
|
||||
//Check if licensed exists
|
||||
if (await imports.which("licensed")) {
|
||||
//Setup for licensed
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > searching dependencies licenses using licensed`)
|
||||
const path = imports.paths.join(imports.os.tmpdir(), `${repository.databaseId}`)
|
||||
//Create temporary directory
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > creating temp dir ${path}`)
|
||||
await imports.fs.rmdir(path, {recursive:true})
|
||||
await imports.fs.mkdir(path, {recursive:true})
|
||||
//Clone repository
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > cloning temp git repository ${repository.url} to ${path}`)
|
||||
const git = imports.git(path)
|
||||
await git.clone(repository.url, path)
|
||||
//Run setup
|
||||
if (setup) {
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > running setup [${setup}]`)
|
||||
await imports.run(setup, {cwd:path}, {prefixed:false})
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
//Create configuration file if needed
|
||||
if (!(await imports.fs.stat(imports.paths.join(path, ".licensed.yml")).then(() => 1).catch(() => 0))) {
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > building .licensed.yml configuration file`)
|
||||
await imports.fs.writeFile(
|
||||
imports.paths.join(path, ".licensed.yml"),
|
||||
[
|
||||
"cache_path: .licensed",
|
||||
].join("\n"),
|
||||
)
|
||||
}
|
||||
else
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > a .licensed.yml configuration file already exists`)
|
||||
|
||||
|
||||
//Spawn licensed process
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > running licensed`)
|
||||
JSON.parse(await imports.run("licensed list --format=json --licenses", {cwd:path})).apps
|
||||
.map(({sources}) => sources?.flatMap(source => source.dependencies.map(({dependency, license}) => {
|
||||
used[license] = (used[license] ?? 0) + 1
|
||||
result.dependencies.push(dependency)
|
||||
result.known += (license in licenses)
|
||||
result.unknown += !(license in licenses)
|
||||
})
|
||||
)
|
||||
)
|
||||
//Cleaning
|
||||
console.debug(`metrics/compute/${login}/plugins > licensed > cleaning temp dir ${path}`)
|
||||
await imports.fs.rmdir(path, {recursive:true})
|
||||
}
|
||||
else
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > licensed not available`)
|
||||
|
||||
|
||||
//List licenses properties
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > compute licenses properties`)
|
||||
const base = {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}))
|
||||
for (const properties of Object.keys(base)) {
|
||||
//Base license
|
||||
if (repository.licenseInfo)
|
||||
licenses[repository.licenseInfo.key]?.[properties]?.map(({key}) => base[properties].add(key))
|
||||
//Combined licenses
|
||||
for (const {key} of detected)
|
||||
licenses[key]?.[properties]?.map(({key}) => combined[properties].add(key))
|
||||
}
|
||||
|
||||
//Merge limitations and 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()
|
||||
//Remove base permissions conflicting with inherited limitations
|
||||
result.permissions = [...base.permissions].filter(key => !combined.limitations.has(key)).map(key => ({key, text:text[key]}))
|
||||
|
||||
//Count used licenses
|
||||
console.debug(`metrics/compute/${login}/plugins > licenses > computing ratio`)
|
||||
const total = Object.values(used).reduce((a, b) => a + b, 0)
|
||||
//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((
|
||||
a,
|
||||
b,
|
||||
) => a.order === b.order ? b.count - a.count : b.order - a.order)
|
||||
for (let i = 0; i < list.length; i++)
|
||||
list[i].x = (list[i - 1]?.x ?? 0) + (list[i - 1]?.value ?? 0)
|
||||
//Save ratios
|
||||
result.list = list
|
||||
|
||||
//Results
|
||||
return result
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
/**Licenses colorizer (based on categorie) */
|
||||
function colors(licenses) {
|
||||
for (const [license, value] of Object.entries(licenses)) {
|
||||
const [permissions, conditions] = [value.permissions, value.conditions].map(properties => properties.map(({key}) => key))
|
||||
switch (true) {
|
||||
//Other licenses
|
||||
case (license === "other"):{
|
||||
value.color = "#8b949e"
|
||||
value.order = 0
|
||||
break
|
||||
}
|
||||
//Strongly protective licenses and network protective
|
||||
case ((conditions.includes("disclose-source"))&&(conditions.includes("same-license"))&&(conditions.includes("network-use-disclose"))):{
|
||||
value.color = "#388bfd"
|
||||
value.order = 1
|
||||
break
|
||||
}
|
||||
//Strongly protective licenses
|
||||
case ((conditions.includes("disclose-source"))&&(conditions.includes("same-license"))):{
|
||||
value.color = "#79c0ff"
|
||||
value.order = 2
|
||||
break
|
||||
}
|
||||
//Weakly protective licenses
|
||||
case ((conditions.includes("disclose-source"))&&(conditions.includes("same-license--library"))):{
|
||||
value.color = "#7ee787"
|
||||
value.order = 3
|
||||
break
|
||||
}
|
||||
//Permissive license
|
||||
case ((permissions.includes("private-use"))&&(permissions.includes("commercial-use"))&&(permissions.includes("modifications"))&&(permissions.includes("distribution"))):{
|
||||
value.color = "#56d364"
|
||||
value.order = 4
|
||||
break
|
||||
}
|
||||
//Unknown
|
||||
default:{
|
||||
value.color = "#6e7681"
|
||||
value.order = -1
|
||||
}
|
||||
function colors(licenses) {
|
||||
for (const [license, value] of Object.entries(licenses)) {
|
||||
const [permissions, conditions] = [value.permissions, value.conditions].map(properties => properties.map(({key}) => key))
|
||||
switch (true) {
|
||||
//Other licenses
|
||||
case (license === "other"): {
|
||||
value.color = "#8b949e"
|
||||
value.order = 0
|
||||
break
|
||||
}
|
||||
//Strongly protective licenses and network protective
|
||||
case ((conditions.includes("disclose-source")) && (conditions.includes("same-license")) && (conditions.includes("network-use-disclose"))): {
|
||||
value.color = "#388bfd"
|
||||
value.order = 1
|
||||
break
|
||||
}
|
||||
//Strongly protective licenses
|
||||
case ((conditions.includes("disclose-source")) && (conditions.includes("same-license"))): {
|
||||
value.color = "#79c0ff"
|
||||
value.order = 2
|
||||
break
|
||||
}
|
||||
//Weakly protective licenses
|
||||
case ((conditions.includes("disclose-source")) && (conditions.includes("same-license--library"))): {
|
||||
value.color = "#7ee787"
|
||||
value.order = 3
|
||||
break
|
||||
}
|
||||
//Permissive license
|
||||
case ((permissions.includes("private-use")) && (permissions.includes("commercial-use")) && (permissions.includes("modifications")) && (permissions.includes("distribution"))): {
|
||||
value.color = "#56d364"
|
||||
value.order = 4
|
||||
break
|
||||
}
|
||||
//Unknown
|
||||
default: {
|
||||
value.color = "#6e7681"
|
||||
value.order = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,46 @@
|
||||
//Setup
|
||||
export default async function({login, data, imports, rest, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.lines))
|
||||
return null
|
||||
export default async function({login, data, imports, rest, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.lines))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {skipped} = imports.metadata.plugins.lines.inputs({data, account, q})
|
||||
skipped.push(...data.shared["repositories.skipped"])
|
||||
//Load inputs
|
||||
let {skipped} = imports.metadata.plugins.lines.inputs({data, account, q})
|
||||
skipped.push(...data.shared["repositories.skipped"])
|
||||
|
||||
//Context
|
||||
let context = {mode:"user"}
|
||||
if (q.repo) {
|
||||
console.debug(`metrics/compute/${login}/plugins > people > switched to repository mode`)
|
||||
context = {...context, mode:"repository"}
|
||||
}
|
||||
|
||||
//Repositories
|
||||
const repositories = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})) ?? []
|
||||
|
||||
//Get contributors stats from repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > lines > querying api`)
|
||||
const lines = {added:0, deleted:0}
|
||||
const response = await Promise.all(repositories.map(({repo, owner}) => (skipped.includes(repo.toLocaleLowerCase()))||(skipped.includes(`${owner}/${repo}`)) ? {} : rest.repos.getContributorsStats({owner, repo})))
|
||||
//Compute changed lines
|
||||
console.debug(`metrics/compute/${login}/plugins > lines > computing total diff`)
|
||||
response.map(({data:repository}) => {
|
||||
//Check if data are available
|
||||
if (!Array.isArray(repository))
|
||||
return
|
||||
//Compute editions
|
||||
const contributors = repository.filter(({author}) => context.mode === "repository" ? true : author?.login === login)
|
||||
for (const contributor of contributors)
|
||||
contributor.weeks.forEach(({a, d}) => (lines.added += a, lines.deleted += d))
|
||||
})
|
||||
|
||||
//Results
|
||||
return lines
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
//Context
|
||||
let context = {mode:"user"}
|
||||
if (q.repo) {
|
||||
console.debug(`metrics/compute/${login}/plugins > people > switched to repository mode`)
|
||||
context = {...context, mode:"repository"}
|
||||
}
|
||||
}
|
||||
|
||||
//Repositories
|
||||
const repositories = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})) ?? []
|
||||
|
||||
//Get contributors stats from repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > lines > querying api`)
|
||||
const lines = {added:0, deleted:0}
|
||||
const response = await Promise.all(repositories.map(({repo, owner}) => (skipped.includes(repo.toLocaleLowerCase())) || (skipped.includes(`${owner}/${repo}`)) ? {} : rest.repos.getContributorsStats({owner, repo})))
|
||||
//Compute changed lines
|
||||
console.debug(`metrics/compute/${login}/plugins > lines > computing total diff`)
|
||||
response.map(({data:repository}) => {
|
||||
//Check if data are available
|
||||
if (!Array.isArray(repository))
|
||||
return
|
||||
//Compute editions
|
||||
const contributors = repository.filter(({author}) => context.mode === "repository" ? true : author?.login === login)
|
||||
for (const contributor of contributors)
|
||||
contributor.weeks.forEach(({a, d}) => (lines.added += a, lines.deleted += d))
|
||||
})
|
||||
|
||||
//Results
|
||||
return lines
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,246 +1,258 @@
|
||||
//Supported providers
|
||||
const providers = {
|
||||
apple:{
|
||||
name:"Apple Music",
|
||||
embed:/^https:..embed.music.apple.com.\w+.playlist/,
|
||||
},
|
||||
spotify:{
|
||||
name:"Spotify",
|
||||
embed:/^https:..open.spotify.com.embed.playlist/,
|
||||
},
|
||||
lastfm:{
|
||||
name:"Last.fm",
|
||||
embed:/^\b$/,
|
||||
},
|
||||
}
|
||||
const providers = {
|
||||
apple:{
|
||||
name:"Apple Music",
|
||||
embed:/^https:..embed.music.apple.com.\w+.playlist/,
|
||||
},
|
||||
spotify:{
|
||||
name:"Spotify",
|
||||
embed:/^https:..open.spotify.com.embed.playlist/,
|
||||
},
|
||||
lastfm:{
|
||||
name:"Last.fm",
|
||||
embed:/^\b$/,
|
||||
},
|
||||
}
|
||||
//Supported modes
|
||||
const modes = {
|
||||
playlist:"Suggested tracks",
|
||||
recent:"Recently played",
|
||||
}
|
||||
const modes = {
|
||||
playlist:"Suggested tracks",
|
||||
recent:"Recently played",
|
||||
}
|
||||
|
||||
//Setup
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = ""} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.music))
|
||||
return null
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = ""} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.music))
|
||||
return null
|
||||
|
||||
//Initialization
|
||||
const raw = {
|
||||
get provider() {
|
||||
return providers[provider]?.name ?? ""
|
||||
},
|
||||
get mode() {
|
||||
return modes[mode] ?? "Unconfigured music plugin"
|
||||
},
|
||||
//Initialization
|
||||
const raw = {
|
||||
get provider() {
|
||||
return providers[provider]?.name ?? ""
|
||||
},
|
||||
get mode() {
|
||||
return modes[mode] ?? "Unconfigured music plugin"
|
||||
},
|
||||
}
|
||||
let tracks = null
|
||||
|
||||
//Load inputs
|
||||
let {provider, mode, playlist, limit, user, "played.at":played_at} = imports.metadata.plugins.music.inputs({data, account, q})
|
||||
//Auto-guess parameters
|
||||
if ((playlist) && (!mode))
|
||||
mode = "playlist"
|
||||
if ((playlist) && (!provider)) {
|
||||
for (const [name, {embed}] of Object.entries(providers)) {
|
||||
if (embed.test(playlist))
|
||||
provider = name
|
||||
}
|
||||
}
|
||||
if (!mode)
|
||||
mode = "recent"
|
||||
//Provider
|
||||
if (!(provider in providers))
|
||||
throw {error:{message:provider ? `Unsupported provider "${provider}"` : "Missing provider"}, ...raw}
|
||||
//Mode
|
||||
if (!(mode in modes))
|
||||
throw {error:{message:`Unsupported mode "${mode}"`}, ...raw}
|
||||
//Playlist mode
|
||||
if (mode === "playlist") {
|
||||
if (!playlist)
|
||||
throw {error:{message:"Missing playlist url"}, ...raw}
|
||||
if (!providers[provider].embed.test(playlist))
|
||||
throw {error:{message:"Unsupported playlist url format"}, ...raw}
|
||||
}
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(100, Number(limit)))
|
||||
|
||||
//Handle mode
|
||||
console.debug(`metrics/compute/${login}/plugins > music > processing mode ${mode} with provider ${provider}`)
|
||||
switch (mode) {
|
||||
//Playlist mode
|
||||
case "playlist": {
|
||||
//Start puppeteer and navigate to playlist
|
||||
console.debug(`metrics/compute/${login}/plugins > music > starting browser`)
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/${login}/plugins > music > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
console.debug(`metrics/compute/${login}/plugins > music > loading page`)
|
||||
await page.goto(playlist)
|
||||
const frame = page.mainFrame()
|
||||
//Handle provider
|
||||
switch (provider) {
|
||||
//Apple music
|
||||
case "apple": {
|
||||
//Parse tracklist
|
||||
await frame.waitForSelector(".tracklist.playlist")
|
||||
tracks = [
|
||||
...await frame.evaluate(() => [...document.querySelectorAll(".tracklist li")].map(li => ({
|
||||
name:li.querySelector(".tracklist__track__name").innerText,
|
||||
artist:li.querySelector(".tracklist__track__sub").innerText,
|
||||
artwork:li.querySelector(".tracklist__track__artwork img").src,
|
||||
}))
|
||||
),
|
||||
]
|
||||
break
|
||||
}
|
||||
let tracks = null
|
||||
|
||||
//Load inputs
|
||||
let {provider, mode, playlist, limit, user, "played.at":played_at} = imports.metadata.plugins.music.inputs({data, account, q})
|
||||
//Auto-guess parameters
|
||||
if ((playlist)&&(!mode))
|
||||
mode = "playlist"
|
||||
if ((playlist)&&(!provider)) {
|
||||
for (const [name, {embed}] of Object.entries(providers)) {
|
||||
if (embed.test(playlist))
|
||||
provider = name
|
||||
}
|
||||
}
|
||||
if (!mode)
|
||||
mode = "recent"
|
||||
//Provider
|
||||
if (!(provider in providers))
|
||||
throw {error:{message:provider ? `Unsupported provider "${provider}"` : "Missing provider"}, ...raw}
|
||||
//Mode
|
||||
if (!(mode in modes))
|
||||
throw {error:{message:`Unsupported mode "${mode}"`}, ...raw}
|
||||
//Playlist mode
|
||||
if (mode === "playlist") {
|
||||
if (!playlist)
|
||||
throw {error:{message:"Missing playlist url"}, ...raw}
|
||||
if (!providers[provider].embed.test(playlist))
|
||||
throw {error:{message:"Unsupported playlist url format"}, ...raw}
|
||||
}
|
||||
//Limit
|
||||
limit = Math.max(1, Math.min(100, Number(limit)))
|
||||
|
||||
//Handle mode
|
||||
console.debug(`metrics/compute/${login}/plugins > music > processing mode ${mode} with provider ${provider}`)
|
||||
switch (mode) {
|
||||
//Playlist mode
|
||||
case "playlist":{
|
||||
//Start puppeteer and navigate to playlist
|
||||
console.debug(`metrics/compute/${login}/plugins > music > starting browser`)
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/${login}/plugins > music > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
console.debug(`metrics/compute/${login}/plugins > music > loading page`)
|
||||
await page.goto(playlist)
|
||||
const frame = page.mainFrame()
|
||||
//Handle provider
|
||||
switch (provider) {
|
||||
//Apple music
|
||||
case "apple":{
|
||||
//Parse tracklist
|
||||
await frame.waitForSelector(".tracklist.playlist")
|
||||
tracks = [...await frame.evaluate(() => [...document.querySelectorAll(".tracklist li")].map(li => ({
|
||||
name:li.querySelector(".tracklist__track__name").innerText,
|
||||
artist:li.querySelector(".tracklist__track__sub").innerText,
|
||||
artwork:li.querySelector(".tracklist__track__artwork img").src,
|
||||
})))]
|
||||
break
|
||||
}
|
||||
//Spotify
|
||||
case "spotify":{
|
||||
//Parse tracklist
|
||||
await frame.waitForSelector("table")
|
||||
tracks = [...await frame.evaluate(() => [...document.querySelectorAll("table tr")].map(tr => ({
|
||||
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
|
||||
artwork:window.getComputedStyle(document.querySelector("button[title=Play]").parentNode, null).backgroundImage.match(/^url\("(?<url>https:...+)"\)$/)?.groups?.url ?? null,
|
||||
})))]
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}" for provider "${provider}"`}, ...raw}
|
||||
}
|
||||
//Close browser
|
||||
console.debug(`metrics/compute/${login}/plugins > music > closing browser`)
|
||||
await browser.close()
|
||||
//Format tracks
|
||||
if (Array.isArray(tracks)) {
|
||||
//Tracks
|
||||
console.debug(`metrics/compute/${login}/plugins > music > found ${tracks.length} tracks`)
|
||||
console.debug(imports.util.inspect(tracks, {depth:Infinity, maxStringLength:256}))
|
||||
//Shuffle tracks
|
||||
tracks = imports.shuffle(tracks)
|
||||
}
|
||||
break
|
||||
}
|
||||
//Recently played
|
||||
case "recent":{
|
||||
//Handle provider
|
||||
switch (provider) {
|
||||
//Spotify
|
||||
case "spotify":{
|
||||
//Prepare credentials
|
||||
const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim())
|
||||
if ((!client_id)||(!client_secret)||(!refresh_token))
|
||||
throw {error:{message:"Spotify token must contain client id/secret and refresh token"}}
|
||||
//API call and parse tracklist
|
||||
try {
|
||||
//Request access token
|
||||
console.debug(`metrics/compute/${login}/plugins > music > requesting access token with spotify refresh token`)
|
||||
const {data:{access_token:access}} = await imports.axios.post("https://accounts.spotify.com/api/token", `${new imports.url.URLSearchParams({grant_type:"refresh_token", refresh_token, client_id, client_secret})}`, {headers:{
|
||||
"Content-Type":"application/x-www-form-urlencoded",
|
||||
}})
|
||||
console.debug(`metrics/compute/${login}/plugins > music > got access token`)
|
||||
//Retrieve tracks
|
||||
console.debug(`metrics/compute/${login}/plugins > music > querying spotify api`)
|
||||
tracks = []
|
||||
for (let hours = .5; hours <= 24; hours++) {
|
||||
//Load track half-hour by half-hour
|
||||
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}`, {headers:{
|
||||
"Content-Type":"application/json",
|
||||
Accept:"application/json",
|
||||
Authorization:`Bearer ${access}`,
|
||||
}})).data.items.map(({track, played_at}) => ({
|
||||
name:track.name,
|
||||
artist:track.artists[0].name,
|
||||
artwork:track.album.images[0].url,
|
||||
played_at:played_at ? `${imports.date(played_at, {timeStyle:"short", timeZone:data.config.timezone?.name})} on ${imports.date(played_at, {dateStyle:"short", timeZone:data.config.timezone?.name})}` : null,
|
||||
}))
|
||||
//Ensure no duplicate are added
|
||||
for (const track of loaded) {
|
||||
if (!tracks.map(({name}) => name).includes(track.name))
|
||||
tracks.push(track)
|
||||
}
|
||||
//Early break
|
||||
if (tracks.length >= limit)
|
||||
break
|
||||
}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response.data?.error_description ?? null
|
||||
const message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
throw {error:{message, instance:error}, ...raw}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
break
|
||||
}
|
||||
//Last.fm
|
||||
case "lastfm":{
|
||||
//API call and parse tracklist
|
||||
try {
|
||||
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`, {headers:{
|
||||
"User-Agent":"lowlighter/metrics",
|
||||
Accept:"application/json",
|
||||
}})).data.recenttracks.track.map(track => ({
|
||||
name:track.name,
|
||||
artist:track.artist["#text"],
|
||||
artwork:track.image.reverse()[0]["#text"],
|
||||
}))
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response.data?.message ?? null
|
||||
const message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
throw {error:{message, instance:error}, ...raw}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}" for provider "${provider}"`}, ...raw}
|
||||
}
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}"`}, ...raw}
|
||||
//Spotify
|
||||
case "spotify": {
|
||||
//Parse tracklist
|
||||
await frame.waitForSelector("table")
|
||||
tracks = [
|
||||
...await frame.evaluate(() => [...document.querySelectorAll("table tr")].map(tr => ({
|
||||
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
|
||||
artwork:window.getComputedStyle(document.querySelector("button[title=Play]").parentNode, null).backgroundImage.match(/^url\("(?<url>https:...+)"\)$/)?.groups?.url ?? null,
|
||||
}))
|
||||
),
|
||||
]
|
||||
break
|
||||
}
|
||||
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}" for provider "${provider}"`}, ...raw}
|
||||
}
|
||||
//Close browser
|
||||
console.debug(`metrics/compute/${login}/plugins > music > closing browser`)
|
||||
await browser.close()
|
||||
//Format tracks
|
||||
if (Array.isArray(tracks)) {
|
||||
//Limit tracklist
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > music > keeping only ${limit} tracks`)
|
||||
tracks.splice(limit)
|
||||
if (Array.isArray(tracks)) {
|
||||
//Tracks
|
||||
console.debug(`metrics/compute/${login}/plugins > music > found ${tracks.length} tracks`)
|
||||
console.debug(imports.util.inspect(tracks, {depth:Infinity, maxStringLength:256}))
|
||||
//Shuffle tracks
|
||||
tracks = imports.shuffle(tracks)
|
||||
}
|
||||
break
|
||||
}
|
||||
//Recently played
|
||||
case "recent": {
|
||||
//Handle provider
|
||||
switch (provider) {
|
||||
//Spotify
|
||||
case "spotify": {
|
||||
//Prepare credentials
|
||||
const [client_id, client_secret, refresh_token] = token.split(",").map(part => part.trim())
|
||||
if ((!client_id) || (!client_secret) || (!refresh_token))
|
||||
throw {error:{message:"Spotify token must contain client id/secret and refresh token"}}
|
||||
//API call and parse tracklist
|
||||
try {
|
||||
//Request access token
|
||||
console.debug(`metrics/compute/${login}/plugins > music > requesting access token with spotify refresh token`)
|
||||
const {data:{access_token:access}} = await imports.axios.post("https://accounts.spotify.com/api/token", `${new imports.url.URLSearchParams({grant_type:"refresh_token", refresh_token, client_id, client_secret})}`, {
|
||||
headers:{
|
||||
"Content-Type":"application/x-www-form-urlencoded",
|
||||
},
|
||||
})
|
||||
console.debug(`metrics/compute/${login}/plugins > music > got access token`)
|
||||
//Retrieve tracks
|
||||
console.debug(`metrics/compute/${login}/plugins > music > querying spotify api`)
|
||||
tracks = []
|
||||
for (let hours = .5; hours <= 24; hours++) {
|
||||
//Load track half-hour by half-hour
|
||||
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}`, {
|
||||
headers:{
|
||||
"Content-Type":"application/json",
|
||||
Accept:"application/json",
|
||||
Authorization:`Bearer ${access}`,
|
||||
},
|
||||
})).data.items.map(({track, played_at}) => ({
|
||||
name:track.name,
|
||||
artist:track.artists[0].name,
|
||||
artwork:track.album.images[0].url,
|
||||
played_at:played_at ? `${imports.date(played_at, {timeStyle:"short", timeZone:data.config.timezone?.name})} on ${imports.date(played_at, {dateStyle:"short", timeZone:data.config.timezone?.name})}` : null,
|
||||
}))
|
||||
//Ensure no duplicate are added
|
||||
for (const track of loaded) {
|
||||
if (!tracks.map(({name}) => name).includes(track.name))
|
||||
tracks.push(track)
|
||||
}
|
||||
//Early break
|
||||
if (tracks.length >= limit)
|
||||
break
|
||||
}
|
||||
//Convert artworks to base64
|
||||
console.debug(`metrics/compute/${login}/plugins > music > loading artworks`)
|
||||
for (const track of tracks) {
|
||||
console.debug(`metrics/compute/${login}/plugins > music > processing ${track.name}`)
|
||||
track.artwork = await imports.imgb64(track.artwork)
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response.data?.error_description ?? null
|
||||
const message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
throw {error:{message, instance:error}, ...raw}
|
||||
}
|
||||
//Save results
|
||||
return {...raw, tracks, played_at}
|
||||
throw error
|
||||
}
|
||||
break
|
||||
}
|
||||
//Last.fm
|
||||
case "lastfm": {
|
||||
//API call and parse tracklist
|
||||
try {
|
||||
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`, {
|
||||
headers:{
|
||||
"User-Agent":"lowlighter/metrics",
|
||||
Accept:"application/json",
|
||||
},
|
||||
})).data.recenttracks.track.map(track => ({
|
||||
name:track.name,
|
||||
artist:track.artist["#text"],
|
||||
artwork:track.image.reverse()[0]["#text"],
|
||||
}))
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response.data?.message ?? null
|
||||
const message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
throw {error:{message, instance:error}, ...raw}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}" for provider "${provider}"`}, ...raw}
|
||||
}
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported mode "${mode}"`}, ...raw}
|
||||
}
|
||||
|
||||
//Unhandled error
|
||||
throw {error:{message:"An error occured (could not retrieve tracks)"}}
|
||||
//Format tracks
|
||||
if (Array.isArray(tracks)) {
|
||||
//Limit tracklist
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > music > keeping only ${limit} tracks`)
|
||||
tracks.splice(limit)
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
//Convert artworks to base64
|
||||
console.debug(`metrics/compute/${login}/plugins > music > loading artworks`)
|
||||
for (const track of tracks) {
|
||||
console.debug(`metrics/compute/${login}/plugins > music > processing ${track.name}`)
|
||||
track.artwork = await imports.imgb64(track.artwork)
|
||||
}
|
||||
//Save results
|
||||
return {...raw, tracks, played_at}
|
||||
}
|
||||
|
||||
//Unhandled error
|
||||
throw {error:{message:"An error occured (could not retrieve tracks)"}}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,53 +1,58 @@
|
||||
//Setup
|
||||
export default async function({q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.nightscout))
|
||||
return null
|
||||
export default async function({q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.nightscout))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {url, datapoints, lowalert, highalert, urgentlowalert, urgenthighalert} = imports.metadata.plugins.nightscout.inputs({data, account, q})
|
||||
//Load inputs
|
||||
let {url, datapoints, lowalert, highalert, urgentlowalert, urgenthighalert} = imports.metadata.plugins.nightscout.inputs({data, account, q})
|
||||
|
||||
if (!url || url === "https://example.herokuapp.com") throw {error:{message:"Nightscout site URL isn't set!"}}
|
||||
if (url.substring(url.length - 1) !== "/") url += "/"
|
||||
if (url.substring(0, 7) === "http://") url = `https://${url.substring(7)}`
|
||||
if (url.substring(0, 8) !== "https://") url = `https://${url}`
|
||||
if (datapoints <= 0) datapoints = 1
|
||||
//Get nightscout data from axios
|
||||
const resp = await imports.axios.get(`${url}api/v1/entries.json?count=${datapoints}`)
|
||||
for (let i = 0; i < resp.data.length; i++){
|
||||
const {sgv} = resp.data[i]
|
||||
//Add human readable timestamps and arrows
|
||||
const date = new Date(resp.data[i].dateString)
|
||||
resp.data[i].arrowHumanReadable = directionArrow(resp.data[i].direction)
|
||||
resp.data[i].timeUTCHumanReadable = `${addZero(date.getUTCHours())}:${addZero(date.getUTCMinutes())}`
|
||||
/*
|
||||
if (!url || url === "https://example.herokuapp.com")
|
||||
throw {error:{message:"Nightscout site URL isn't set!"}}
|
||||
if (url.substring(url.length - 1) !== "/")
|
||||
url += "/"
|
||||
if (url.substring(0, 7) === "http://")
|
||||
url = `https://${url.substring(7)}`
|
||||
if (url.substring(0, 8) !== "https://")
|
||||
url = `https://${url}`
|
||||
if (datapoints <= 0)
|
||||
datapoints = 1
|
||||
//Get nightscout data from axios
|
||||
const resp = await imports.axios.get(`${url}api/v1/entries.json?count=${datapoints}`)
|
||||
for (let i = 0; i < resp.data.length; i++) {
|
||||
const {sgv} = resp.data[i]
|
||||
//Add human readable timestamps and arrows
|
||||
const date = new Date(resp.data[i].dateString)
|
||||
resp.data[i].arrowHumanReadable = directionArrow(resp.data[i].direction)
|
||||
resp.data[i].timeUTCHumanReadable = `${addZero(date.getUTCHours())}:${addZero(date.getUTCMinutes())}`
|
||||
/*
|
||||
* Add colors and alert names
|
||||
* TODO: Maybe make colors better themed instead of just the "github style" - red and yellow could fit better than darker shades of green
|
||||
*/
|
||||
let color = "#40c463"
|
||||
let alertName = "Normal"
|
||||
if (sgv >= urgenthighalert || sgv <= urgentlowalert){
|
||||
color = "#216e39"
|
||||
alertName = sgv >= urgenthighalert ? "Urgent High" : "Urgent Low"
|
||||
}
|
||||
else if (sgv >= highalert || sgv <= lowalert){
|
||||
color = "#30a14e"
|
||||
alertName = sgv >= highalert ? "High" : "Low"
|
||||
}
|
||||
resp.data[i].color = color
|
||||
resp.data[i].alert = alertName
|
||||
}
|
||||
return {data:resp.data.reverse()}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
let color = "#40c463"
|
||||
let alertName = "Normal"
|
||||
if (sgv >= urgenthighalert || sgv <= urgentlowalert) {
|
||||
color = "#216e39"
|
||||
alertName = sgv >= urgenthighalert ? "Urgent High" : "Urgent Low"
|
||||
}
|
||||
else if (sgv >= highalert || sgv <= lowalert) {
|
||||
color = "#30a14e"
|
||||
alertName = sgv >= highalert ? "High" : "Low"
|
||||
}
|
||||
resp.data[i].color = color
|
||||
resp.data[i].alert = alertName
|
||||
}
|
||||
return {data:resp.data.reverse()}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
function addZero(i) {
|
||||
if (i < 10)
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, graphql, data, account, queries}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.notable))
|
||||
return null
|
||||
export default async function({login, q, imports, graphql, data, account, queries}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.notable))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {filter, repositories} = imports.metadata.plugins.notable.inputs({data, account, q})
|
||||
//Load inputs
|
||||
let {filter, repositories} = imports.metadata.plugins.notable.inputs({data, account, q})
|
||||
|
||||
//Initialization
|
||||
const organizations = new Map()
|
||||
//Initialization
|
||||
const organizations = new Map()
|
||||
|
||||
//Iterate through contributed repositories from organizations
|
||||
{
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
console.debug(`metrics/compute/${login}/plugins > notable > retrieving contributed repositories after ${cursor}`)
|
||||
const {user:{repositoriesContributedTo:{edges}}} = await graphql(queries.notable.contributions({login, after:cursor ? `after: "${cursor}"` : "", repositories:100}))
|
||||
cursor = edges?.[edges?.length-1]?.cursor
|
||||
edges
|
||||
.filter(({node}) => node.isInOrganization)
|
||||
.filter(({node}) => imports.ghfilter(filter, {name:node.nameWithOwner, stars:node.stargazers.totalCount, watchers:node.watchers.totalCount, forks:node.forks.totalCount}))
|
||||
.map(({node}) => organizations.set(repositories ? node.nameWithOwner : node.owner.login, node.owner.avatarUrl))
|
||||
pushed = edges.length
|
||||
} while ((pushed)&&(cursor))
|
||||
}
|
||||
//Iterate through contributed repositories from organizations
|
||||
{
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
console.debug(`metrics/compute/${login}/plugins > notable > retrieving contributed repositories after ${cursor}`)
|
||||
const {user:{repositoriesContributedTo:{edges}}} = await graphql(queries.notable.contributions({login, after:cursor ? `after: "${cursor}"` : "", repositories:100}))
|
||||
cursor = edges?.[edges?.length - 1]?.cursor
|
||||
edges
|
||||
.filter(({node}) => node.isInOrganization)
|
||||
.filter(({node}) => imports.ghfilter(filter, {name:node.nameWithOwner, stars:node.stargazers.totalCount, watchers:node.watchers.totalCount, forks:node.forks.totalCount}))
|
||||
.map(({node}) => organizations.set(repositories ? node.nameWithOwner : node.owner.login, node.owner.avatarUrl))
|
||||
pushed = edges.length
|
||||
} while ((pushed) && (cursor))
|
||||
}
|
||||
|
||||
//Set contributions
|
||||
const contributions = (await Promise.all([...organizations.entries()].map(async([name, avatarUrl]) => ({name, avatar:await imports.imgb64(avatarUrl)})))).sort((a, b) => a.name.localeCompare(b.name))
|
||||
console.debug(`metrics/compute/${login}/plugins > notable > found contributions to ${organizations.length} organizations`)
|
||||
//Set contributions
|
||||
const contributions = (await Promise.all([...organizations.entries()].map(async ([name, avatarUrl]) => ({name, avatar:await imports.imgb64(avatarUrl)})))).sort((a, b) => a.name.localeCompare(b.name))
|
||||
console.debug(`metrics/compute/${login}/plugins > notable > found contributions to ${organizations.length} organizations`)
|
||||
|
||||
//Results
|
||||
return {contributions}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {contributions}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
//Setup
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = null} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.pagespeed)||((!data.user.websiteUrl)&&(!q["pagespeed.url"])))
|
||||
return null
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = null} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.pagespeed) || ((!data.user.websiteUrl) && (!q["pagespeed.url"])))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {detailed, screenshot, url} = imports.metadata.plugins.pagespeed.inputs({data, account, q})
|
||||
//Format url if needed
|
||||
if (!/^https?:[/][/]/.test(url))
|
||||
url = `https://${url}`
|
||||
const {protocol, host} = imports.url.parse(url)
|
||||
const result = {url:`${protocol}//${host}`, detailed, scores:[], metrics:{}}
|
||||
//Load scores from API
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > querying api for ${result.url}`)
|
||||
const scores = new Map()
|
||||
await Promise.all(["performance", "accessibility", "best-practices", "seo"].map(async category => {
|
||||
//Perform audit
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performing audit ${category}`)
|
||||
const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}${token ? `&key=${token}` : ""}`)
|
||||
const {score, title} = request.data.lighthouseResult.categories[category]
|
||||
scores.set(category, {score, title})
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
|
||||
//Store screenshot
|
||||
if ((screenshot)&&(category === "performance")) {
|
||||
result.screenshot = request.data.lighthouseResult.audits["final-screenshot"].details.data
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
|
||||
}
|
||||
}))
|
||||
result.scores = [scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]
|
||||
|
||||
//Detailed metrics
|
||||
if (detailed) {
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performing detailed audit`)
|
||||
const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?&url=${url}${token ? `&key=${token}` : ""}`)
|
||||
Object.assign(result.metrics, ...request.data.lighthouseResult.audits.metrics.details.items)
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed detailed audit (status code ${request.status})`)
|
||||
}
|
||||
|
||||
//Results
|
||||
return result
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.error?.message?.match(/Lighthouse returned error: (?<description>[A-Z_]+)/)?.groups?.description ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
//Load inputs
|
||||
let {detailed, screenshot, url} = imports.metadata.plugins.pagespeed.inputs({data, account, q})
|
||||
//Format url if needed
|
||||
if (!/^https?:[/][/]/.test(url))
|
||||
url = `https://${url}`
|
||||
const {protocol, host} = imports.url.parse(url)
|
||||
const result = {url:`${protocol}//${host}`, detailed, scores:[], metrics:{}}
|
||||
//Load scores from API
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > querying api for ${result.url}`)
|
||||
const scores = new Map()
|
||||
await Promise.all(["performance", "accessibility", "best-practices", "seo"].map(async category => {
|
||||
//Perform audit
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performing audit ${category}`)
|
||||
const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?category=${category}&url=${url}${token ? `&key=${token}` : ""}`)
|
||||
const {score, title} = request.data.lighthouseResult.categories[category]
|
||||
scores.set(category, {score, title})
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
|
||||
//Store screenshot
|
||||
if ((screenshot) && (category === "performance")) {
|
||||
result.screenshot = request.data.lighthouseResult.audits["final-screenshot"].details.data
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed audit ${category} (status code ${request.status})`)
|
||||
}
|
||||
}))
|
||||
result.scores = [scores.get("performance"), scores.get("accessibility"), scores.get("best-practices"), scores.get("seo")]
|
||||
|
||||
//Detailed metrics
|
||||
if (detailed) {
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performing detailed audit`)
|
||||
const request = await imports.axios.get(`https://www.googleapis.com/pagespeedonline/v5/runPagespeed?&url=${url}${token ? `&key=${token}` : ""}`)
|
||||
Object.assign(result.metrics, ...request.data.lighthouseResult.audits.metrics.details.items)
|
||||
console.debug(`metrics/compute/${login}/plugins > pagespeed > performed detailed audit (status code ${request.status})`)
|
||||
}
|
||||
|
||||
//Results
|
||||
return result
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.error?.message?.match(/Lighthouse returned error: (?<description>[A-Z_]+)/)?.groups?.description ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,96 +1,98 @@
|
||||
//Setup
|
||||
export default async function({login, data, graphql, rest, q, queries, imports, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.people))
|
||||
return null
|
||||
export default async function({login, data, graphql, rest, q, queries, imports, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.people))
|
||||
return null
|
||||
|
||||
//Context
|
||||
let context = {
|
||||
mode:"user",
|
||||
types:account === "organization" ? ["sponsorshipsAsMaintainer", "sponsorshipsAsSponsor", "membersWithRole", "thanks"] : ["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor", "thanks"],
|
||||
default:"followers, following",
|
||||
alias:{followed:"following", sponsors:"sponsorshipsAsMaintainer", sponsored:"sponsorshipsAsSponsor", sponsoring:"sponsorshipsAsSponsor", members:"membersWithRole"},
|
||||
sponsorships:{sponsorshipsAsMaintainer:"sponsorEntity", sponsorshipsAsSponsor:"sponsorable"},
|
||||
}
|
||||
if (q.repo) {
|
||||
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()
|
||||
context = {...context, mode:"repository", types:["contributors", "stargazers", "watchers", "sponsorshipsAsMaintainer", "thanks"], default:"stargazers, watchers", owner, repo}
|
||||
}
|
||||
//Context
|
||||
let context = {
|
||||
mode:"user",
|
||||
types:account === "organization" ? ["sponsorshipsAsMaintainer", "sponsorshipsAsSponsor", "membersWithRole", "thanks"] : ["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor", "thanks"],
|
||||
default:"followers, following",
|
||||
alias:{followed:"following", sponsors:"sponsorshipsAsMaintainer", sponsored:"sponsorshipsAsSponsor", sponsoring:"sponsorshipsAsSponsor", members:"membersWithRole"},
|
||||
sponsorships:{sponsorshipsAsMaintainer:"sponsorEntity", sponsorshipsAsSponsor:"sponsorable"},
|
||||
}
|
||||
if (q.repo) {
|
||||
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()
|
||||
context = {...context, mode:"repository", types:["contributors", "stargazers", "watchers", "sponsorshipsAsMaintainer", "thanks"], default:"stargazers, watchers", owner, repo}
|
||||
}
|
||||
|
||||
//Load inputs
|
||||
let {limit, types, size, identicons, thanks, shuffle, "sponsors.custom":_sponsors} = imports.metadata.plugins.people.inputs({data, account, q}, {types:context.default})
|
||||
//Filter types
|
||||
types = [...new Set([...types].map(type => (context.alias[type] ?? type)).filter(type => context.types.includes(type)) ?? [])]
|
||||
if ((types.includes("sponsorshipsAsMaintainer"))&&(_sponsors?.length)) {
|
||||
types.unshift("sponsorshipsCustom")
|
||||
data.user.sponsorshipsAsMaintainer.totalCount += _sponsors.length
|
||||
}
|
||||
//Load inputs
|
||||
let {limit, types, size, identicons, thanks, shuffle, "sponsors.custom":_sponsors} = imports.metadata.plugins.people.inputs({data, account, q}, {types:context.default})
|
||||
//Filter types
|
||||
types = [...new Set([...types].map(type => (context.alias[type] ?? type)).filter(type => context.types.includes(type)) ?? [])]
|
||||
if ((types.includes("sponsorshipsAsMaintainer")) && (_sponsors?.length)) {
|
||||
types.unshift("sponsorshipsCustom")
|
||||
data.user.sponsorshipsAsMaintainer.totalCount += _sponsors.length
|
||||
}
|
||||
|
||||
//Retrieve followers from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > people > querying api`)
|
||||
const result = Object.fromEntries(types.map(type => [type, []]))
|
||||
for (const type of types) {
|
||||
//Iterate through people
|
||||
console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type}`)
|
||||
//Rest
|
||||
if (type === "contributors") {
|
||||
const {owner, repo} = context
|
||||
const {data:nodes} = await rest.repos.listContributors({owner, repo})
|
||||
result[type].push(...nodes.map(({login, avatar_url}) => ({login, avatarUrl:avatar_url})))
|
||||
}
|
||||
else if ((type === "thanks")||(type === "sponsorshipsCustom")) {
|
||||
const users = {thanks, sponsorshipsCustom:_sponsors}[type] ?? []
|
||||
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})))
|
||||
}
|
||||
//GraphQL
|
||||
else {
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type} after ${cursor}`)
|
||||
const {[type]:{edges}} = (
|
||||
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] :
|
||||
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({login, type, size, after:cursor ? `after: "${cursor}"` : "", account})))[account]
|
||||
)
|
||||
cursor = edges?.[edges?.length-1]?.cursor
|
||||
result[type].push(...edges.map(({node}) => node[context.sponsorships[type]] ?? node))
|
||||
pushed = edges.length
|
||||
} while ((pushed)&&(cursor)&&((limit === 0)||(result[type].length <= (shuffle ? 10*limit : limit))))
|
||||
}
|
||||
//Shuffle
|
||||
if (shuffle) {
|
||||
console.debug(`metrics/compute/${login}/plugins > people > shuffling`)
|
||||
imports.shuffle(result[type])
|
||||
}
|
||||
//Limit people
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > people > keeping only ${limit} ${type}`)
|
||||
result[type].splice(limit)
|
||||
}
|
||||
//Hide real avator with identicons if enabled
|
||||
if (identicons) {
|
||||
console.debug(`metrics/compute/${login}/plugins > people > using identicons`)
|
||||
result[type].map(user => user.avatarUrl = `https://github.com/identicons/${user.login}.png`)
|
||||
}
|
||||
//Convert avatars to base64
|
||||
console.debug(`metrics/compute/${login}/plugins > people > loading avatars`)
|
||||
await Promise.all(result[type].map(async user => user.avatar = await imports.imgb64(user.avatarUrl)))
|
||||
}
|
||||
|
||||
//Special type handling
|
||||
if (types.includes("sponsorshipsCustom"))
|
||||
types.splice(types.indexOf("sponsorshipsCustom"), 1)
|
||||
|
||||
//Results
|
||||
return {types, size, ...result}
|
||||
//Retrieve followers from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > people > querying api`)
|
||||
const result = Object.fromEntries(types.map(type => [type, []]))
|
||||
for (const type of types) {
|
||||
//Iterate through people
|
||||
console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type}`)
|
||||
//Rest
|
||||
if (type === "contributors") {
|
||||
const {owner, repo} = context
|
||||
const {data:nodes} = await rest.repos.listContributors({owner, repo})
|
||||
result[type].push(...nodes.map(({login, avatar_url}) => ({login, avatarUrl:avatar_url})))
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
else if ((type === "thanks") || (type === "sponsorshipsCustom")) {
|
||||
const users = {thanks, sponsorshipsCustom:_sponsors}[type] ?? []
|
||||
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})))
|
||||
}
|
||||
//GraphQL
|
||||
else {
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type} after ${cursor}`)
|
||||
const {[type]:{edges}} = (
|
||||
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]
|
||||
: 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({login, type, size, after:cursor ? `after: "${cursor}"` : "", account})))[account]
|
||||
)
|
||||
cursor = edges?.[edges?.length - 1]?.cursor
|
||||
result[type].push(...edges.map(({node}) => node[context.sponsorships[type]] ?? node))
|
||||
pushed = edges.length
|
||||
} while ((pushed) && (cursor) && ((limit === 0) || (result[type].length <= (shuffle ? 10 * limit : limit))))
|
||||
}
|
||||
//Shuffle
|
||||
if (shuffle) {
|
||||
console.debug(`metrics/compute/${login}/plugins > people > shuffling`)
|
||||
imports.shuffle(result[type])
|
||||
}
|
||||
//Limit people
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > people > keeping only ${limit} ${type}`)
|
||||
result[type].splice(limit)
|
||||
}
|
||||
//Hide real avator with identicons if enabled
|
||||
if (identicons) {
|
||||
console.debug(`metrics/compute/${login}/plugins > people > using identicons`)
|
||||
result[type].map(user => user.avatarUrl = `https://github.com/identicons/${user.login}.png`)
|
||||
}
|
||||
//Convert avatars to base64
|
||||
console.debug(`metrics/compute/${login}/plugins > people > loading avatars`)
|
||||
await Promise.all(result[type].map(async user => user.avatar = await imports.imgb64(user.avatarUrl)))
|
||||
}
|
||||
|
||||
//Special type handling
|
||||
if (types.includes("sponsorshipsCustom"))
|
||||
types.splice(types.indexOf("sponsorshipsCustom"), 1)
|
||||
|
||||
//Results
|
||||
return {types, size, ...result}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,62 @@
|
||||
//Setup
|
||||
export default async function({login, data, imports, q, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.posts))
|
||||
return null
|
||||
export default async function({login, data, imports, q, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.posts))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {source, descriptions, covers, limit, user} = imports.metadata.plugins.posts.inputs({data, account, q})
|
||||
//Load inputs
|
||||
let {source, descriptions, covers, limit, user} = imports.metadata.plugins.posts.inputs({data, account, q})
|
||||
|
||||
//Retrieve posts
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > processing with source ${source}`)
|
||||
let posts = null
|
||||
let link = null
|
||||
switch (source) {
|
||||
//Dev.to
|
||||
case "dev.to":{
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > querying api`)
|
||||
posts = (await imports.axios.get(`https://dev.to/api/articles?username=${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}`
|
||||
break
|
||||
}
|
||||
//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(({title, brief:description, dateAdded:date, coverImage:image, slug}) => ({title, description, date, image, link:`https://hashnode.com/post/${slug}`}))
|
||||
link = `https://hashnode.com/@${user}`
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported source "${source}"`}}
|
||||
}
|
||||
|
||||
//Format posts
|
||||
if (Array.isArray(posts)) {
|
||||
//Limit posts
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > keeping only ${limit} posts`)
|
||||
posts.splice(limit)
|
||||
}
|
||||
//Cover images
|
||||
if (covers) {
|
||||
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})))
|
||||
}
|
||||
//Results
|
||||
return {source, link, descriptions, covers, list:posts}
|
||||
}
|
||||
|
||||
//Unhandled error
|
||||
throw {error:{message:"An error occured (could not retrieve posts)"}}
|
||||
//Retrieve posts
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > processing with source ${source}`)
|
||||
let posts = null
|
||||
let link = null
|
||||
switch (source) {
|
||||
//Dev.to
|
||||
case "dev.to": {
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > querying api`)
|
||||
posts = (await imports.axios.get(`https://dev.to/api/articles?username=${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}`
|
||||
break
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
//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((
|
||||
{title, brief:description, dateAdded:date, coverImage:image, slug},
|
||||
) => ({title, description, date, image, link:`https://hashnode.com/post/${slug}`}))
|
||||
link = `https://hashnode.com/@${user}`
|
||||
break
|
||||
}
|
||||
//Unsupported
|
||||
default:
|
||||
throw {error:{message:`Unsupported source "${source}"`}}
|
||||
}
|
||||
|
||||
//Format posts
|
||||
if (Array.isArray(posts)) {
|
||||
//Limit posts
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > posts > keeping only ${limit} posts`)
|
||||
posts.splice(limit)
|
||||
}
|
||||
//Cover images
|
||||
if (covers) {
|
||||
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})))
|
||||
}
|
||||
//Results
|
||||
return {source, link, descriptions, covers, list:posts}
|
||||
}
|
||||
|
||||
//Unhandled error
|
||||
throw {error:{message:"An error occured (could not retrieve posts)"}}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
//Setup
|
||||
export default async function({login, data, imports, graphql, q, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.projects))
|
||||
return null
|
||||
export default async function({login, data, imports, graphql, q, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.projects))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {limit, repositories, descriptions} = imports.metadata.plugins.projects.inputs({data, account, q})
|
||||
//Repositories projects
|
||||
repositories = repositories.filter(repository => /[-\w]+[/][-\w]+[/]projects[/]\d+/.test(repository))
|
||||
//Update limit if repositories projects were specified manually
|
||||
limit = Math.max(repositories.length, limit)
|
||||
//Load inputs
|
||||
let {limit, repositories, descriptions} = imports.metadata.plugins.projects.inputs({data, account, q})
|
||||
//Repositories projects
|
||||
repositories = repositories.filter(repository => /[-\w]+[/][-\w]+[/]projects[/]\d+/.test(repository))
|
||||
//Update limit if repositories projects were specified manually
|
||||
limit = Math.max(repositories.length, limit)
|
||||
|
||||
//Retrieve user owned projects from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > querying api`)
|
||||
const {[account]:{projects}} = await graphql(queries.projects.user({login, limit, account}))
|
||||
//Retrieve user owned projects from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > querying api`)
|
||||
const {[account]:{projects}} = await graphql(queries.projects.user({login, limit, account}))
|
||||
|
||||
//Retrieve repositories projects from graphql api
|
||||
for (const identifier of repositories) {
|
||||
//Querying repository project
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > querying api for ${identifier}`)
|
||||
const {user, repository, id} = identifier.match(/(?<user>[-\w]+)[/](?<repository>[-\w]+)[/]projects[/](?<id>\d+)/)?.groups ?? {}
|
||||
let project = null
|
||||
for (const account of ["user", "organization"]) {
|
||||
try {
|
||||
({project} = (await graphql(queries.projects.repository({user, repository, id, account})))[account].repository)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
if (!project)
|
||||
throw new Error(`Could not load project ${user}/${repository}`)
|
||||
//Adding it to projects list
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > registering ${identifier}`)
|
||||
project.name = `${project.name} (${user}/${repository})`
|
||||
projects.nodes.unshift(project)
|
||||
projects.totalCount++
|
||||
}
|
||||
|
||||
//Iterate through projects and format them
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > processing ${projects.nodes.length} projects`)
|
||||
const list = []
|
||||
for (const project of projects.nodes) {
|
||||
//Format date
|
||||
const time = (Date.now()-new Date(project.updatedAt).getTime())/(24*60*60*1000)
|
||||
let updated = new Date(project.updatedAt).toDateString().substring(4)
|
||||
if (time < 1)
|
||||
updated = "less than 1 day ago"
|
||||
else if (time < 30)
|
||||
updated = `${Math.floor(time)} day${time >= 2 ? "s" : ""} ago`
|
||||
//Format progress
|
||||
const {enabled, todoCount:todo, inProgressCount:doing, doneCount:done} = project.progress
|
||||
//Append
|
||||
list.push({name:project.name, updated, description:project.body, progress:{enabled, todo, doing, done, total:todo+doing+done}})
|
||||
}
|
||||
|
||||
//Limit
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > keeping only ${limit} projects`)
|
||||
list.splice(limit)
|
||||
|
||||
//Results
|
||||
return {list, totalCount:projects.totalCount, descriptions}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.errors?.map(({type}) => type)?.includes("INSUFFICIENT_SCOPES"))
|
||||
message = "Insufficient token rights"
|
||||
throw {error:{message, instance:error}}
|
||||
//Retrieve repositories projects from graphql api
|
||||
for (const identifier of repositories) {
|
||||
//Querying repository project
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > querying api for ${identifier}`)
|
||||
const {user, repository, id} = identifier.match(/(?<user>[-\w]+)[/](?<repository>[-\w]+)[/]projects[/](?<id>\d+)/)?.groups ?? {}
|
||||
let project = null
|
||||
for (const account of ["user", "organization"]) {
|
||||
try {
|
||||
({project} = (await graphql(queries.projects.repository({user, repository, id, account})))[account].repository)
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
if (!project)
|
||||
throw new Error(`Could not load project ${user}/${repository}`)
|
||||
//Adding it to projects list
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > registering ${identifier}`)
|
||||
project.name = `${project.name} (${user}/${repository})`
|
||||
projects.nodes.unshift(project)
|
||||
projects.totalCount++
|
||||
}
|
||||
|
||||
//Iterate through projects and format them
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > processing ${projects.nodes.length} projects`)
|
||||
const list = []
|
||||
for (const project of projects.nodes) {
|
||||
//Format date
|
||||
const time = (Date.now() - new Date(project.updatedAt).getTime()) / (24 * 60 * 60 * 1000)
|
||||
let updated = new Date(project.updatedAt).toDateString().substring(4)
|
||||
if (time < 1)
|
||||
updated = "less than 1 day ago"
|
||||
else if (time < 30)
|
||||
updated = `${Math.floor(time)} day${time >= 2 ? "s" : ""} ago`
|
||||
//Format progress
|
||||
const {enabled, todoCount:todo, inProgressCount:doing, doneCount:done} = project.progress
|
||||
//Append
|
||||
list.push({name:project.name, updated, description:project.body, progress:{enabled, todo, doing, done, total:todo + doing + done}})
|
||||
}
|
||||
|
||||
//Limit
|
||||
console.debug(`metrics/compute/${login}/plugins > projects > keeping only ${limit} projects`)
|
||||
list.splice(limit)
|
||||
|
||||
//Results
|
||||
return {list, totalCount:projects.totalCount, descriptions}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.errors?.map(({type}) => type)?.includes("INSUFFICIENT_SCOPES"))
|
||||
message = "Insufficient token rights"
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.reactions))
|
||||
return null
|
||||
export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.reactions))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {limit, days, details, display, ignored} = imports.metadata.plugins.reactions.inputs({data, account, q})
|
||||
//Load inputs
|
||||
let {limit, days, details, display, ignored} = imports.metadata.plugins.reactions.inputs({data, account, q})
|
||||
|
||||
//Load issue comments
|
||||
let cursor = null, pushed = 0
|
||||
const comments = []
|
||||
for (const type of ["issues", "issueComments"]) {
|
||||
do {
|
||||
//Load issue comments
|
||||
let cursor = null, pushed = 0
|
||||
const comments = []
|
||||
for (const type of ["issues", "issueComments"]) {
|
||||
do {
|
||||
//Load issue comments
|
||||
console.debug(`metrics/compute/${login}/plugins > reactions > retrieving ${type} after ${cursor}`)
|
||||
const {user:{[type]:{edges}}} = await graphql(queries.reactions({login, type, after:cursor ? `after: "${cursor}"` : ""}))
|
||||
cursor = edges?.[edges?.length-1]?.cursor
|
||||
//Save issue comments
|
||||
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)}))
|
||||
.filter(comment => Number.isFinite(days) ? comment.created < new Date(Date.now()-days*24*60*60*1000) : true)
|
||||
pushed = filtered.length
|
||||
comments.push(...filtered)
|
||||
console.debug(`metrics/compute/${login}/plugins > reactions > currently at ${comments.length} comments`)
|
||||
//Early break
|
||||
if ((comments.length >= limit)||(filtered.length < edges.length))
|
||||
break
|
||||
} while ((cursor)&&(pushed)&&(comments.length < limit))
|
||||
}
|
||||
console.debug(`metrics/compute/${login}/plugins > reactions > retrieving ${type} after ${cursor}`)
|
||||
const {user:{[type]:{edges}}} = await graphql(queries.reactions({login, type, after:cursor ? `after: "${cursor}"` : ""}))
|
||||
cursor = edges?.[edges?.length - 1]?.cursor
|
||||
//Save issue comments
|
||||
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)}))
|
||||
.filter(comment => Number.isFinite(days) ? comment.created < new Date(Date.now() - days * 24 * 60 * 60 * 1000) : true)
|
||||
pushed = filtered.length
|
||||
comments.push(...filtered)
|
||||
console.debug(`metrics/compute/${login}/plugins > reactions > currently at ${comments.length} comments`)
|
||||
//Early break
|
||||
if ((comments.length >= limit) || (filtered.length < edges.length))
|
||||
break
|
||||
} while ((cursor) && (pushed) && (comments.length < limit))
|
||||
}
|
||||
|
||||
//Applying limit
|
||||
if (limit) {
|
||||
comments.splice(limit)
|
||||
console.debug(`metrics/compute/${login}/plugins > reactions > keeping only ${comments.length} comments`)
|
||||
}
|
||||
//Applying limit
|
||||
if (limit) {
|
||||
comments.splice(limit)
|
||||
console.debug(`metrics/compute/${login}/plugins > reactions > keeping only ${comments.length} comments`)
|
||||
}
|
||||
|
||||
//Format reactions list
|
||||
const list = {}
|
||||
const reactions = comments.flatMap(({reactions}) => reactions)
|
||||
for (const reaction of reactions)
|
||||
list[reaction] = (list[reaction] ?? 0) + 1
|
||||
const max = Math.max(...Object.values(list))
|
||||
for (const [key, value] of Object.entries(list))
|
||||
list[key] = {value, percentage:value/reactions.length, score:value/(display === "relative" ? max : reactions.length)}
|
||||
//Format reactions list
|
||||
const list = {}
|
||||
const reactions = comments.flatMap(({reactions}) => reactions)
|
||||
for (const reaction of reactions)
|
||||
list[reaction] = (list[reaction] ?? 0) + 1
|
||||
const max = Math.max(...Object.values(list))
|
||||
for (const [key, value] of Object.entries(list))
|
||||
list[key] = {value, percentage:value / reactions.length, score:value / (display === "relative" ? max : reactions.length)}
|
||||
|
||||
//Results
|
||||
return {list, comments:comments.length, details, days, twemoji:q["config.twemoji"]}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {list, comments:comments.length, details, days, twemoji:q["config.twemoji"]}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.rss))
|
||||
return null
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.rss))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {source, limit} = imports.metadata.plugins.rss.inputs({data, account, q})
|
||||
if (!source)
|
||||
throw {error:{message:"A RSS feed is required"}}
|
||||
//Load inputs
|
||||
let {source, limit} = imports.metadata.plugins.rss.inputs({data, account, q})
|
||||
if (!source)
|
||||
throw {error:{message:"A RSS feed is required"}}
|
||||
|
||||
//Load rss feed
|
||||
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)}))
|
||||
//Load rss feed
|
||||
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)}))
|
||||
|
||||
//Limit feed
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > rss > keeping only ${limit} items`)
|
||||
feed.splice(limit)
|
||||
}
|
||||
//Limit feed
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > rss > keeping only ${limit} items`)
|
||||
feed.splice(limit)
|
||||
}
|
||||
|
||||
//Results
|
||||
return {source:title, description, link, feed}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {source:title, description, link, feed}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.screenshot))
|
||||
return null
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.screenshot))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {url, selector, title, background} = imports.metadata.plugins.screenshot.inputs({data, account, q})
|
||||
if (!url)
|
||||
throw {error:{message:"An url is required"}}
|
||||
//Load inputs
|
||||
let {url, selector, title, background} = imports.metadata.plugins.screenshot.inputs({data, account, q})
|
||||
if (!url)
|
||||
throw {error:{message:"An url is required"}}
|
||||
|
||||
//Start puppeteer and navigate to page
|
||||
console.debug(`metrics/compute/${login}/plugins > screenshot > starting browser`)
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/${login}/plugins > screenshot > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
await page.setViewport({width:1280, height:1280})
|
||||
console.debug(`metrics/compute/${login}/plugins > screenshot > loading ${url}`)
|
||||
await page.goto(url)
|
||||
//Start puppeteer and navigate to page
|
||||
console.debug(`metrics/compute/${login}/plugins > screenshot > starting browser`)
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/${login}/plugins > screenshot > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
await page.setViewport({width:1280, height:1280})
|
||||
console.debug(`metrics/compute/${login}/plugins > screenshot > loading ${url}`)
|
||||
await page.goto(url)
|
||||
|
||||
//Screenshot
|
||||
await page.waitForSelector(selector)
|
||||
const clip = await page.evaluate(selector => {
|
||||
const {x, y, width, height} = document.querySelector(selector).getBoundingClientRect()
|
||||
return {x, y, width, height}
|
||||
}, selector)
|
||||
console.debug(`metrics/compute/${login}/plugins > screenshot > coordinates ${JSON.stringify(clip)}`)
|
||||
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, clip.width), imports.jimp.AUTO)
|
||||
await browser.close()
|
||||
//Screenshot
|
||||
await page.waitForSelector(selector)
|
||||
const clip = await page.evaluate(selector => {
|
||||
const {x, y, width, height} = document.querySelector(selector).getBoundingClientRect()
|
||||
return {x, y, width, height}
|
||||
}, selector)
|
||||
console.debug(`metrics/compute/${login}/plugins > screenshot > coordinates ${JSON.stringify(clip)}`)
|
||||
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, clip.width), imports.jimp.AUTO)
|
||||
await browser.close()
|
||||
|
||||
//Results
|
||||
return {image:await screenshot.getBase64Async("image/png"), title, height:screenshot.bitmap.height, width:screenshot.bitmap.width}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {title:"Screenshot error", error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {image:await screenshot.getBase64Async("image/png"), title, height:screenshot.bitmap.height, width:screenshot.bitmap.width}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {title:"Screenshot error", error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,47 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.skyline))
|
||||
return null
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.skyline))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {year, frames, quality, compatibility} = imports.metadata.plugins.skyline.inputs({data, account, q})
|
||||
if (Number.isNaN(year)) {
|
||||
year = new Date().getFullYear()
|
||||
console.debug(`metrics/compute/${login}/plugins > skyline > year set to ${year}`)
|
||||
}
|
||||
const width = 454
|
||||
const height = 284
|
||||
//Load inputs
|
||||
let {year, frames, quality, compatibility} = imports.metadata.plugins.skyline.inputs({data, account, q})
|
||||
if (Number.isNaN(year)) {
|
||||
year = new Date().getFullYear()
|
||||
console.debug(`metrics/compute/${login}/plugins > skyline > year set to ${year}`)
|
||||
}
|
||||
const width = 454
|
||||
const height = 284
|
||||
|
||||
//Start puppeteer and navigate to skyline.github.com
|
||||
console.debug(`metrics/compute/${login}/plugins > skyline > starting browser`)
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/${login}/plugins > skyline > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
await page.setViewport({width, height})
|
||||
//Start puppeteer and navigate to skyline.github.com
|
||||
console.debug(`metrics/compute/${login}/plugins > skyline > starting browser`)
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/${login}/plugins > skyline > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
await page.setViewport({width, height})
|
||||
|
||||
//Load page
|
||||
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})
|
||||
console.debug(`metrics/compute/${login}/plugins > skyline > waiting for initial render`)
|
||||
const frame = page.mainFrame()
|
||||
await page.waitForFunction('[...document.querySelectorAll("span")].map(span => span.innerText).includes("Download STL file")', {timeout:90*1000})
|
||||
await frame.evaluate(() => [...document.querySelectorAll("button, footer, a")].map(element => element.remove()))
|
||||
//Load page
|
||||
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})
|
||||
console.debug(`metrics/compute/${login}/plugins > skyline > waiting for initial render`)
|
||||
const frame = page.mainFrame()
|
||||
await page.waitForFunction('[...document.querySelectorAll("span")].map(span => span.innerText).includes("Download STL file")', {timeout:90 * 1000})
|
||||
await frame.evaluate(() => [...document.querySelectorAll("button, footer, a")].map(element => element.remove()))
|
||||
|
||||
//Generate gif
|
||||
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)})
|
||||
//Generate gif
|
||||
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)})
|
||||
|
||||
//Close puppeteer
|
||||
await browser.close()
|
||||
//Close puppeteer
|
||||
await browser.close()
|
||||
|
||||
//Results
|
||||
return {animation, width, height, compatibility}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {animation, width, height, compatibility}
|
||||
}
|
||||
|
||||
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,101 +1,150 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.stackoverflow))
|
||||
return null
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.stackoverflow))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {sections, user, limit, lines, "lines.snippet":codelines} = imports.metadata.plugins.stackoverflow.inputs({data, account, q})
|
||||
if (!user)
|
||||
throw {error:{message:"You must provide a stackoverflow user id"}}
|
||||
//Load inputs
|
||||
let {sections, user, limit, lines, "lines.snippet":codelines} = imports.metadata.plugins.stackoverflow.inputs({data, account, q})
|
||||
if (!user)
|
||||
throw {error:{message:"You must provide a stackoverflow user id"}}
|
||||
|
||||
//Initialization
|
||||
//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 filters = {user:"!0Z-LvgkLYnTCu1858)*D0lcx2", answer:"!7goY5TLWwCz.BaGpe)tv5C6Bks2q8siMH6", question:"!)EhwvzgX*hrClxjLzqxiZHHbTPRE5Pb3B9vvRaqCx5-ZY.vPr"}
|
||||
const result = {sections, lines}
|
||||
//Initialization
|
||||
//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 filters = {user:"!0Z-LvgkLYnTCu1858)*D0lcx2", answer:"!7goY5TLWwCz.BaGpe)tv5C6Bks2q8siMH6", question:"!)EhwvzgX*hrClxjLzqxiZHHbTPRE5Pb3B9vvRaqCx5-ZY.vPr"}
|
||||
const result = {sections, lines}
|
||||
|
||||
//Stackoverflow user metrics
|
||||
{
|
||||
//Account metrics
|
||||
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:{total:comments}} = await imports.axios.get(`${api.user}/comments?site=stackoverflow&filter=total`)
|
||||
//Save result
|
||||
result.user = {reputation, badges:bronze+silver+gold, questions, answers, comments, views}
|
||||
}
|
||||
//Stackoverflow user metrics
|
||||
{
|
||||
//Account metrics
|
||||
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:{total:comments}} = await imports.axios.get(`${api.user}/comments?site=stackoverflow&filter=total`)
|
||||
//Save result
|
||||
result.user = {reputation, badges:bronze + silver + gold, questions, answers, comments, views}
|
||||
}
|
||||
|
||||
//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))) {
|
||||
//Load and format answers
|
||||
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}`)
|
||||
result[key] = await Promise.all(items.map(item => format.answer(item, {imports, data, codelines})))
|
||||
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loaded ${result[key].length} items`)
|
||||
//Load related questions
|
||||
const ids = result[key].map(({question_id}) => question_id).filter(id => id)
|
||||
if (ids) {
|
||||
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}`)
|
||||
await Promise.all(items.map(item => format.question(item, {imports, data, codelines})))
|
||||
}
|
||||
}
|
||||
|
||||
//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))) {
|
||||
//Load and format questions
|
||||
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}`)
|
||||
result[key] = await Promise.all(items.map(item => format.question(item, {imports, data, codelines})))
|
||||
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loaded ${result[key].length} items`)
|
||||
//Load related answers
|
||||
const ids = result[key].map(({accepted_answer_id}) => accepted_answer_id).filter(id => id)
|
||||
if (ids) {
|
||||
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}`)
|
||||
await Promise.all(items.map(item => format.answer(item, {imports, data, codelines})))
|
||||
}
|
||||
}
|
||||
|
||||
//Results
|
||||
return result
|
||||
//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))) {
|
||||
//Load and format answers
|
||||
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}`)
|
||||
result[key] = await Promise.all(items.map(item => format.answer(item, {imports, data, codelines})))
|
||||
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loaded ${result[key].length} items`)
|
||||
//Load related questions
|
||||
const ids = result[key].map(({question_id}) => question_id).filter(id => id)
|
||||
if (ids) {
|
||||
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}`)
|
||||
await Promise.all(items.map(item => format.question(item, {imports, data, codelines})))
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
|
||||
//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))) {
|
||||
//Load and format questions
|
||||
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}`)
|
||||
result[key] = await Promise.all(items.map(item => format.question(item, {imports, data, codelines})))
|
||||
console.debug(`metrics/compute/${login}/plugins > stackoverflow > loaded ${result[key].length} items`)
|
||||
//Load related answers
|
||||
const ids = result[key].map(({accepted_answer_id}) => accepted_answer_id).filter(id => id)
|
||||
if (ids) {
|
||||
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}`)
|
||||
await Promise.all(items.map(item => format.answer(item, {imports, data, codelines})))
|
||||
}
|
||||
}
|
||||
|
||||
//Results
|
||||
return result
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
//Formatters
|
||||
const format = {
|
||||
/**Cached */
|
||||
cached:new Map(),
|
||||
/**Format stackoverflow code snippets */
|
||||
code(text) {
|
||||
return text.replace(/<!-- language: lang-(?<lang>\w+) -->\s*(?<snippet> {4}[\s\S]+?)(?=(?:<!-- end snippet -->)|(?:<!-- language: lang-))/g, "```$<lang>\n$<snippet>```")
|
||||
const format = {
|
||||
/**Cached */
|
||||
cached:new Map(),
|
||||
/**Format stackoverflow code snippets */
|
||||
code(text) {
|
||||
return text.replace(/<!-- language: lang-(?<lang>\w+) -->\s*(?<snippet> {4}[\s\S]+?)(?=(?:<!-- end snippet -->)|(?:<!-- language: lang-))/g, "```$<lang>\n$<snippet>```")
|
||||
},
|
||||
/**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, data, codelines}) {
|
||||
const formatted = {
|
||||
type:"answer",
|
||||
body:await imports.markdown(format.code(imports.htmlunescape(body)), {codelines}),
|
||||
score,
|
||||
upvotes,
|
||||
downvotes,
|
||||
accepted,
|
||||
comments,
|
||||
author,
|
||||
created:imports.date(creation_date * 1000, {dateStyle:"short", timeZone:data.config.timezone?.name}),
|
||||
link,
|
||||
id,
|
||||
question_id,
|
||||
get question() {
|
||||
return format.cached.get(`q${this.question_id}`) ?? null
|
||||
},
|
||||
/**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, data, codelines}) {
|
||||
const formatted = {type:"answer", body:await imports.markdown(format.code(imports.htmlunescape(body)), {codelines}), score, upvotes, downvotes, accepted, comments, author, created:imports.date(creation_date*1000, {dateStyle:"short", timeZone:data.config.timezone?.name}), link, id, question_id,
|
||||
get question() {
|
||||
return format.cached.get(`q${this.question_id}`) ?? null
|
||||
},
|
||||
}
|
||||
this.cached.set(`a${id}`, formatted)
|
||||
return formatted
|
||||
}
|
||||
this.cached.set(`a${id}`, formatted)
|
||||
return formatted
|
||||
},
|
||||
/**Format questions */
|
||||
async question(
|
||||
{
|
||||
title,
|
||||
body_markdown:body,
|
||||
score,
|
||||
up_vote_count:upvotes,
|
||||
down_vote_count:downvotes,
|
||||
favorite_count:favorites,
|
||||
tags,
|
||||
is_answered:answered,
|
||||
answer_count:answers,
|
||||
comment_count:comments,
|
||||
view_count:views,
|
||||
creation_date,
|
||||
owner:{display_name:author},
|
||||
link,
|
||||
question_id:id,
|
||||
accepted_answer_id = null,
|
||||
},
|
||||
{imports, data, codelines},
|
||||
) {
|
||||
const formatted = {
|
||||
type:"question",
|
||||
title:await imports.markdown(title),
|
||||
body:await imports.markdown(format.code(imports.htmlunescape(body)), {codelines}),
|
||||
score,
|
||||
upvotes,
|
||||
downvotes,
|
||||
favorites,
|
||||
tags,
|
||||
answered,
|
||||
answers,
|
||||
comments,
|
||||
views,
|
||||
author,
|
||||
created:imports.date(creation_date * 1000, {dateStyle:"short", timeZone:data.config.timezone?.name}),
|
||||
link,
|
||||
id,
|
||||
accepted_answer_id,
|
||||
get answer() {
|
||||
return format.cached.get(`a${this.accepted_answer_id}`) ?? null
|
||||
},
|
||||
/**Format questions */
|
||||
async question({title, body_markdown:body, score, up_vote_count:upvotes, down_vote_count:downvotes, favorite_count:favorites, tags, is_answered:answered, answer_count:answers, comment_count:comments, view_count:views, creation_date, owner:{display_name:author}, link, question_id:id, accepted_answer_id = null}, {imports, data, codelines}) {
|
||||
const formatted = {type:"question", title:await imports.markdown(title), body:await imports.markdown(format.code(imports.htmlunescape(body)), {codelines}), score, upvotes, downvotes, favorites, tags, answered, answers, comments, views, author, created:imports.date(creation_date*1000, {dateStyle:"short", timeZone:data.config.timezone?.name}), link, id, accepted_answer_id,
|
||||
get answer() {
|
||||
return format.cached.get(`a${this.accepted_answer_id}`) ?? null
|
||||
},
|
||||
}
|
||||
this.cached.set(`q${id}`, formatted)
|
||||
return formatted
|
||||
},
|
||||
}
|
||||
}
|
||||
this.cached.set(`q${id}`, formatted)
|
||||
return formatted
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
//Setup
|
||||
export default async function({login, graphql, data, imports, q, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.stargazers))
|
||||
return null
|
||||
export default async function({login, graphql, data, imports, q, queries, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.stargazers))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
imports.metadata.plugins.stargazers.inputs({data, account, q})
|
||||
//Load inputs
|
||||
imports.metadata.plugins.stargazers.inputs({data, account, q})
|
||||
|
||||
//Retrieve stargazers from graphql 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 dates = []
|
||||
for (const {repository, owner} of repositories) {
|
||||
//Iterate through stargazers
|
||||
console.debug(`metrics/compute/${login}/plugins > stargazers > retrieving stargazers of ${repository}`)
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
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}"` : ""}))
|
||||
cursor = edges?.[edges?.length-1]?.cursor
|
||||
dates.push(...edges.map(({starredAt}) => new Date(starredAt)))
|
||||
pushed = edges.length
|
||||
} while ((pushed)&&(cursor))
|
||||
//Limit repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers for ${repository}`)
|
||||
}
|
||||
console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers in total`)
|
||||
//Retrieve stargazers from graphql 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 dates = []
|
||||
for (const {repository, owner} of repositories) {
|
||||
//Iterate through stargazers
|
||||
console.debug(`metrics/compute/${login}/plugins > stargazers > retrieving stargazers of ${repository}`)
|
||||
let cursor = null
|
||||
let pushed = 0
|
||||
do {
|
||||
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}"` : ""}))
|
||||
cursor = edges?.[edges?.length - 1]?.cursor
|
||||
dates.push(...edges.map(({starredAt}) => new Date(starredAt)))
|
||||
pushed = edges.length
|
||||
} while ((pushed) && (cursor))
|
||||
//Limit repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers for ${repository}`)
|
||||
}
|
||||
console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers in total`)
|
||||
|
||||
//Compute stargazers increments
|
||||
const days = 14
|
||||
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
|
||||
.map(date => date.toISOString().slice(0, 10))
|
||||
.filter(date => date in increments.dates)
|
||||
.map(date => increments.dates[date]++)
|
||||
increments.min = Math.min(...Object.values(increments.dates))
|
||||
increments.max = Math.max(...Object.values(increments.dates))
|
||||
//Compute stargazers increments
|
||||
const days = 14
|
||||
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
|
||||
.map(date => date.toISOString().slice(0, 10))
|
||||
.filter(date => date in increments.dates)
|
||||
.map(date => increments.dates[date]++)
|
||||
increments.min = Math.min(...Object.values(increments.dates))
|
||||
increments.max = Math.max(...Object.values(increments.dates))
|
||||
|
||||
//Compute total stargazers
|
||||
let {stargazers} = data.computed.repositories
|
||||
const total = {dates:{...increments.dates}, max:NaN, min:NaN}
|
||||
{
|
||||
const dates = Object.keys(total.dates)
|
||||
for (let i = dates.length-1; i >= 0; i--) {
|
||||
const date = dates[i], tomorrow = dates[i+1]
|
||||
stargazers -= (increments.dates[tomorrow] ?? 0)
|
||||
total.dates[date] = stargazers
|
||||
}
|
||||
}
|
||||
total.min = Math.min(...Object.values(total.dates))
|
||||
total.max = Math.max(...Object.values(total.dates))
|
||||
|
||||
//Months name
|
||||
const months = ["", "Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."]
|
||||
|
||||
//Results
|
||||
return {total, increments, months}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
//Compute total stargazers
|
||||
let {stargazers} = data.computed.repositories
|
||||
const total = {dates:{...increments.dates}, max:NaN, min:NaN}
|
||||
{
|
||||
const dates = Object.keys(total.dates)
|
||||
for (let i = dates.length - 1; i >= 0; i--) {
|
||||
const date = dates[i], tomorrow = dates[i + 1]
|
||||
stargazers -= (increments.dates[tomorrow] ?? 0)
|
||||
total.dates[date] = stargazers
|
||||
}
|
||||
}
|
||||
total.min = Math.min(...Object.values(total.dates))
|
||||
total.max = Math.max(...Object.values(total.dates))
|
||||
|
||||
//Months name
|
||||
const months = ["", "Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."]
|
||||
|
||||
//Results
|
||||
return {total, increments, months}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
//Setup
|
||||
export default async function({login, data, graphql, q, queries, imports, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.stars))
|
||||
return null
|
||||
export default async function({login, data, graphql, q, queries, imports, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.stars))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {limit} = imports.metadata.plugins.stars.inputs({data, account, q})
|
||||
//Load inputs
|
||||
let {limit} = imports.metadata.plugins.stars.inputs({data, account, q})
|
||||
|
||||
//Retrieve user stars from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > stars > querying api`)
|
||||
const {user:{starredRepositories:{edges:repositories}}} = await graphql(queries.stars({login, limit}))
|
||||
//Retrieve user stars from graphql api
|
||||
console.debug(`metrics/compute/${login}/plugins > stars > querying api`)
|
||||
const {user:{starredRepositories:{edges:repositories}}} = await graphql(queries.stars({login, limit}))
|
||||
|
||||
//Format starred repositories
|
||||
for (const edge of repositories) {
|
||||
//Format date
|
||||
const time = (Date.now()-new Date(edge.starredAt).getTime())/(24*60*60*1000)
|
||||
let updated = new Date(edge.starredAt).toDateString().substring(4)
|
||||
if (time < 1)
|
||||
updated = `${Math.ceil(time*24)} hour${Math.ceil(time*24) >= 2 ? "s" : ""} ago`
|
||||
else if (time < 30)
|
||||
updated = `${Math.floor(time)} day${time >= 2 ? "s" : ""} ago`
|
||||
edge.starred = updated
|
||||
}
|
||||
//Format starred repositories
|
||||
for (const edge of repositories) {
|
||||
//Format date
|
||||
const time = (Date.now() - new Date(edge.starredAt).getTime()) / (24 * 60 * 60 * 1000)
|
||||
let updated = new Date(edge.starredAt).toDateString().substring(4)
|
||||
if (time < 1)
|
||||
updated = `${Math.ceil(time * 24)} hour${Math.ceil(time * 24) >= 2 ? "s" : ""} ago`
|
||||
else if (time < 30)
|
||||
updated = `${Math.floor(time)} day${time >= 2 ? "s" : ""} ago`
|
||||
edge.starred = updated
|
||||
}
|
||||
|
||||
//Results
|
||||
return {repositories}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {repositories}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, token} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.stock))
|
||||
return null
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, token} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.stock))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {symbol, interval, duration} = imports.metadata.plugins.stock.inputs({data, account, q})
|
||||
if (!token)
|
||||
throw {error:{message:"A token is required"}}
|
||||
if (!symbol)
|
||||
throw {error:{message:"A company stock symbol is required"}}
|
||||
symbol = symbol.toLocaleUpperCase()
|
||||
//Load inputs
|
||||
let {symbol, interval, duration} = imports.metadata.plugins.stock.inputs({data, account, q})
|
||||
if (!token)
|
||||
throw {error:{message:"A token is required"}}
|
||||
if (!symbol)
|
||||
throw {error:{message:"A company stock symbol is required"}}
|
||||
symbol = symbol.toLocaleUpperCase()
|
||||
|
||||
//Query API for company informations
|
||||
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", {
|
||||
params:{symbol, region:"US"},
|
||||
headers:{"x-rapidapi-key":token},
|
||||
})
|
||||
//Query API for company informations
|
||||
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", {
|
||||
params:{symbol, region:"US"},
|
||||
headers:{"x-rapidapi-key":token},
|
||||
})
|
||||
|
||||
//Query API for sotck charts
|
||||
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", {
|
||||
params:{interval, symbol, range:duration, region:"US"},
|
||||
headers:{"x-rapidapi-key":token},
|
||||
})
|
||||
const {currency, regularMarketPrice:price, previousClose:previous} = meta
|
||||
//Query API for sotck charts
|
||||
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", {
|
||||
params:{interval, symbol, range:duration, region:"US"},
|
||||
headers:{"x-rapidapi-key":token},
|
||||
})
|
||||
const {currency, regularMarketPrice:price, previousClose:previous} = meta
|
||||
|
||||
//Generating chart
|
||||
console.debug(`metrics/compute/${login}/plugins > stock > generating chart`)
|
||||
const chart = await imports.chartist("line", {
|
||||
width:480,
|
||||
height:160,
|
||||
showPoint:false,
|
||||
axisX:{showGrid:false, labelInterpolationFnc:(value, index) => index%Math.floor(close.length/4) === 0 ? value : null},
|
||||
axisY:{scaleMinSpace:20},
|
||||
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))),
|
||||
series:[close],
|
||||
})
|
||||
//Generating chart
|
||||
console.debug(`metrics/compute/${login}/plugins > stock > generating chart`)
|
||||
const chart = await imports.chartist("line", {
|
||||
width:480,
|
||||
height:160,
|
||||
showPoint:false,
|
||||
axisX:{showGrid:false, labelInterpolationFnc:(value, index) => index % Math.floor(close.length / 4) === 0 ? value : null},
|
||||
axisY:{scaleMinSpace:20},
|
||||
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))),
|
||||
series:[close],
|
||||
})
|
||||
|
||||
//Results
|
||||
return {chart, currency, price, previous, delta:price-previous, symbol, company, interval, duration}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.message ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {chart, currency, price, previous, delta:price - previous, symbol, company, interval, duration}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.message ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,81 +1,87 @@
|
||||
//Setup
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
export default async function({login, q, imports, data, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.support))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
imports.metadata.plugins.stackoverflow.inputs({data, account, q})
|
||||
|
||||
//Start puppeteer and navigate to github.community
|
||||
const result = {stats:{solutions:0, posts:0, topics:0, received:0}, badges:{count:0}}
|
||||
console.debug(`metrics/compute/${login}/plugins > support > starting browser`)
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/${login}/plugins > support > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
|
||||
//Check account existence
|
||||
{
|
||||
await page.goto(`https://github.community/u/${login}`)
|
||||
const frame = page.mainFrame()
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.support))
|
||||
return null
|
||||
await frame.waitForSelector(".user-profile-names", {timeout:5000})
|
||||
}
|
||||
catch {
|
||||
throw {error:{message:"Could not find matching account on github.community"}}
|
||||
}
|
||||
}
|
||||
|
||||
//Load inputs
|
||||
imports.metadata.plugins.stackoverflow.inputs({data, account, q})
|
||||
|
||||
//Start puppeteer and navigate to github.community
|
||||
const result = {stats:{solutions:0, posts:0, topics:0, received:0}, badges:{count:0}}
|
||||
console.debug(`metrics/compute/${login}/plugins > support > starting browser`)
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/${login}/plugins > support > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
|
||||
//Check account existence
|
||||
{
|
||||
await page.goto(`https://github.community/u/${login}`)
|
||||
const frame = page.mainFrame()
|
||||
try {
|
||||
await frame.waitForSelector(".user-profile-names", {timeout:5000})
|
||||
}
|
||||
catch {
|
||||
throw {error:{message:"Could not find matching account on github.community"}}
|
||||
}
|
||||
}
|
||||
|
||||
//Stats
|
||||
{
|
||||
await page.goto(`https://github.community/u/${login}/summary`)
|
||||
const frame = page.mainFrame()
|
||||
await frame.waitForSelector(".stats-section")
|
||||
Object.assign(result.stats, Object.fromEntries((await frame.evaluate(() => [...document.querySelectorAll(".stats-section li")].map(el => [
|
||||
//Stats
|
||||
{
|
||||
await page.goto(`https://github.community/u/${login}/summary`)
|
||||
const frame = page.mainFrame()
|
||||
await frame.waitForSelector(".stats-section")
|
||||
Object.assign(
|
||||
result.stats,
|
||||
Object.fromEntries(
|
||||
(await frame.evaluate(() => [...document.querySelectorAll(".stats-section li")].map(el => [
|
||||
el.querySelector(".label").innerText.trim().toLocaleLowerCase(),
|
||||
el.querySelector(".value").innerText.trim().toLocaleLowerCase(),
|
||||
]))).map(([key, value]) => {
|
||||
switch (true) {
|
||||
case /solutions?/.test(key):
|
||||
return ["solutions", Number(value)]
|
||||
case /posts? created/.test(key):
|
||||
return ["posts", Number(value)]
|
||||
case /topics? created/.test(key):
|
||||
return ["topics", Number(value)]
|
||||
case /received/.test(key):
|
||||
return ["hearts", Number(value)]
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}).filter(kv => kv)))
|
||||
}
|
||||
])
|
||||
)).map(([key, value]) => {
|
||||
switch (true) {
|
||||
case /solutions?/.test(key):
|
||||
return ["solutions", Number(value)]
|
||||
case /posts? created/.test(key):
|
||||
return ["posts", Number(value)]
|
||||
case /topics? created/.test(key):
|
||||
return ["topics", Number(value)]
|
||||
case /received/.test(key):
|
||||
return ["hearts", Number(value)]
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}).filter(kv => kv),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
//Badges
|
||||
{
|
||||
await page.goto(`https://github.community/u/${login}/badges`)
|
||||
const frame = page.mainFrame()
|
||||
await frame.waitForSelector(".user-badges-list")
|
||||
const badges = await frame.evaluate(() => ({
|
||||
uniques:[...document.querySelectorAll(".badge-card .badge-link")].map(el => 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)
|
||||
result.badges = badges
|
||||
}
|
||||
//Badges
|
||||
{
|
||||
await page.goto(`https://github.community/u/${login}/badges`)
|
||||
const frame = page.mainFrame()
|
||||
await frame.waitForSelector(".user-badges-list")
|
||||
const badges = await frame.evaluate(() => ({
|
||||
uniques:[...document.querySelectorAll(".badge-card .badge-link")].map(el => 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)
|
||||
result.badges = badges
|
||||
}
|
||||
|
||||
//Close browser
|
||||
console.debug(`metrics/compute/${login}/plugins > support > closing browser`)
|
||||
await browser.close()
|
||||
//Close browser
|
||||
console.debug(`metrics/compute/${login}/plugins > support > closing browser`)
|
||||
await browser.close()
|
||||
|
||||
//Results
|
||||
return result
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
//Results
|
||||
return result
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,95 +1,96 @@
|
||||
//Setup
|
||||
export default async function({login, data, imports, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.topics))
|
||||
return null
|
||||
export default async function({login, data, imports, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.topics))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {sort, mode, limit} = imports.metadata.plugins.topics.inputs({data, account, q})
|
||||
const shuffle = (sort === "random")
|
||||
//Load inputs
|
||||
let {sort, mode, limit} = imports.metadata.plugins.topics.inputs({data, account, q})
|
||||
const shuffle = (sort === "random")
|
||||
|
||||
//Start puppeteer and navigate to topics
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > searching starred topics`)
|
||||
let topics = []
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > starting browser`)
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
//Start puppeteer and navigate to topics
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > searching starred topics`)
|
||||
let topics = []
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > starting browser`)
|
||||
const browser = await imports.puppeteer.launch()
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > started ${await browser.version()}`)
|
||||
const page = await browser.newPage()
|
||||
|
||||
//Iterate through pages
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
//Load page
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > loading page ${i}`)
|
||||
await page.goto(`https://github.com/stars/${login}/topics?direction=desc&page=${i}&sort=${sort}`)
|
||||
const frame = page.mainFrame()
|
||||
//Extract topics
|
||||
await Promise.race([frame.waitForSelector("ul.repo-list"), frame.waitForSelector(".blankslate")])
|
||||
const starred = await frame.evaluate(() => [...document.querySelectorAll("ul.repo-list li")].map(li => ({
|
||||
name:li.querySelector(".f3").innerText,
|
||||
description:li.querySelector(".f5").innerText,
|
||||
icon:li.querySelector("img")?.src ?? null,
|
||||
})))
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > extracted ${starred.length} starred topics`)
|
||||
//Check if next page exists
|
||||
if (!starred.length) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > no more page to load`)
|
||||
break
|
||||
}
|
||||
topics.push(...starred)
|
||||
}
|
||||
|
||||
//Close browser
|
||||
console.debug(`metrics/compute/${login}/plugins > music > closing browser`)
|
||||
await browser.close()
|
||||
|
||||
//Shuffle topics
|
||||
if (shuffle) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > shuffling topics`)
|
||||
topics = imports.shuffle(topics)
|
||||
}
|
||||
|
||||
//Limit topics (starred mode)
|
||||
if ((mode === "starred")&&(limit > 0)) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
|
||||
const removed = topics.splice(limit)
|
||||
if (removed.length)
|
||||
topics.push({name:`And ${removed.length} more...`, description:removed.map(({name}) => name).join(", "), icon:null})
|
||||
}
|
||||
|
||||
//Convert icons to base64
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > loading artworks`)
|
||||
for (const topic of topics) {
|
||||
if (topic.icon) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > processing ${topic.name}`)
|
||||
const {icon} = topic
|
||||
topic.icon = await imports.imgb64(icon)
|
||||
topic.icon24 = await imports.imgb64(icon, {force:true, width:24, height:24})
|
||||
}
|
||||
//Escape HTML description
|
||||
topic.description = imports.htmlescape(topic.description)
|
||||
}
|
||||
|
||||
//Filter topics with icon (mastered mode)
|
||||
if (mode === "mastered") {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > filtering topics with icon`)
|
||||
topics = topics.filter(({icon}) => icon)
|
||||
}
|
||||
|
||||
//Limit topics (mastered mode)
|
||||
if ((mode === "mastered")&&(limit > 0)) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
|
||||
topics.splice(limit)
|
||||
}
|
||||
|
||||
//Results
|
||||
return {mode, list:topics}
|
||||
//Iterate through pages
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
//Load page
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > loading page ${i}`)
|
||||
await page.goto(`https://github.com/stars/${login}/topics?direction=desc&page=${i}&sort=${sort}`)
|
||||
const frame = page.mainFrame()
|
||||
//Extract topics
|
||||
await Promise.race([frame.waitForSelector("ul.repo-list"), frame.waitForSelector(".blankslate")])
|
||||
const starred = await frame.evaluate(() => [...document.querySelectorAll("ul.repo-list li")].map(li => ({
|
||||
name:li.querySelector(".f3").innerText,
|
||||
description:li.querySelector(".f5").innerText,
|
||||
icon:li.querySelector("img")?.src ?? null,
|
||||
}))
|
||||
)
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > extracted ${starred.length} starred topics`)
|
||||
//Check if next page exists
|
||||
if (!starred.length) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > no more page to load`)
|
||||
break
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
topics.push(...starred)
|
||||
}
|
||||
|
||||
//Close browser
|
||||
console.debug(`metrics/compute/${login}/plugins > music > closing browser`)
|
||||
await browser.close()
|
||||
|
||||
//Shuffle topics
|
||||
if (shuffle) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > shuffling topics`)
|
||||
topics = imports.shuffle(topics)
|
||||
}
|
||||
|
||||
//Limit topics (starred mode)
|
||||
if ((mode === "starred") && (limit > 0)) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
|
||||
const removed = topics.splice(limit)
|
||||
if (removed.length)
|
||||
topics.push({name:`And ${removed.length} more...`, description:removed.map(({name}) => name).join(", "), icon:null})
|
||||
}
|
||||
|
||||
//Convert icons to base64
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > loading artworks`)
|
||||
for (const topic of topics) {
|
||||
if (topic.icon) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > processing ${topic.name}`)
|
||||
const {icon} = topic
|
||||
topic.icon = await imports.imgb64(icon)
|
||||
topic.icon24 = await imports.imgb64(icon, {force:true, width:24, height:24})
|
||||
}
|
||||
//Escape HTML description
|
||||
topic.description = imports.htmlescape(topic.description)
|
||||
}
|
||||
|
||||
//Filter topics with icon (mastered mode)
|
||||
if (mode === "mastered") {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > filtering topics with icon`)
|
||||
topics = topics.filter(({icon}) => icon)
|
||||
}
|
||||
|
||||
//Limit topics (mastered mode)
|
||||
if ((mode === "mastered") && (limit > 0)) {
|
||||
console.debug(`metrics/compute/${login}/plugins > topics > keeping only ${limit} topics`)
|
||||
topics.splice(limit)
|
||||
}
|
||||
|
||||
//Results
|
||||
return {mode, list:topics}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
if (error.error?.message)
|
||||
throw error
|
||||
throw {error:{message:"An error occured", instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
//Setup
|
||||
export default async function({login, imports, data, rest, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.traffic))
|
||||
return null
|
||||
export default async function({login, imports, data, rest, q, account}, {enabled = false} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.traffic))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
imports.metadata.plugins.traffic.inputs({data, account, q})
|
||||
//Load inputs
|
||||
imports.metadata.plugins.traffic.inputs({data, account, q})
|
||||
|
||||
//Repositories
|
||||
const repositories = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})) ?? []
|
||||
//Repositories
|
||||
const repositories = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})) ?? []
|
||||
|
||||
//Get views stats from repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > traffic > querying api`)
|
||||
const views = {count:0, uniques:0}
|
||||
const response = await Promise.all(repositories.map(({repo, owner}) => rest.repos.getViews({owner, repo})))
|
||||
//Get views stats from repositories
|
||||
console.debug(`metrics/compute/${login}/plugins > traffic > querying api`)
|
||||
const views = {count:0, uniques:0}
|
||||
const response = await Promise.all(repositories.map(({repo, owner}) => rest.repos.getViews({owner, repo})))
|
||||
|
||||
//Compute views
|
||||
console.debug(`metrics/compute/${login}/plugins > traffic > computing stats`)
|
||||
response.filter(({data}) => data).map(({data:{count, uniques}}) => (views.count += count, views.uniques += uniques))
|
||||
//Compute views
|
||||
console.debug(`metrics/compute/${login}/plugins > traffic > computing stats`)
|
||||
response.filter(({data}) => data).map(({data:{count, uniques}}) => (views.count += count, views.uniques += uniques))
|
||||
|
||||
//Results
|
||||
return {views}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.status === 403)
|
||||
message = "Insufficient token rights"
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
//Results
|
||||
return {views}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.status === 403)
|
||||
message = "Insufficient token rights"
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +1,105 @@
|
||||
//Setup
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = ""} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled)||(!q.tweets))
|
||||
return null
|
||||
export default async function({login, imports, data, q, account}, {enabled = false, token = ""} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
if ((!enabled) || (!q.tweets))
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let {limit, user:username, attachments} = imports.metadata.plugins.tweets.inputs({data, account, q})
|
||||
//Load inputs
|
||||
let {limit, user:username, attachments} = imports.metadata.plugins.tweets.inputs({data, account, q})
|
||||
|
||||
//Load user profile
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > loading twitter profile (@${username})`)
|
||||
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 user profile
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > loading twitter profile (@${username})`)
|
||||
const {data:{data:profile = null}} = await imports.axios.get(`https://api.twitter.com/2/users/by/username/${username}?user.fields=profile_image_url,verified`, {headers:{Authorization:`Bearer ${token}`}})
|
||||
|
||||
//Load profile image
|
||||
if (profile?.profile_image_url) {
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > loading profile image`)
|
||||
profile.profile_image = await imports.imgb64(profile.profile_image_url)
|
||||
}
|
||||
//Load profile image
|
||||
if (profile?.profile_image_url) {
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > loading profile image`)
|
||||
profile.profile_image = await imports.imgb64(profile.profile_image_url)
|
||||
}
|
||||
|
||||
//Load tweets
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > querying api`)
|
||||
const {data:{data:tweets = [], includes:{media = []} = {}}} = await imports.axios.get(`https://api.twitter.com/2/tweets/search/recent?query=from:${username}&tweet.fields=created_at,entities&media.fields=preview_image_url,url,type&expansions=entities.mentions.username,attachments.media_keys`, {headers:{Authorization:`Bearer ${token}`}})
|
||||
const medias = new Map(media.map(({media_key, type, url, preview_image_url}) => [media_key, (type === "photo")||(type === "animated_gif") ? url : type === "video" ? preview_image_url : null]))
|
||||
//Load tweets
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > querying api`)
|
||||
const {data:{data:tweets = [], includes:{media = []} = {}}} = await imports.axios.get(
|
||||
`https://api.twitter.com/2/tweets/search/recent?query=from:${username}&tweet.fields=created_at,entities&media.fields=preview_image_url,url,type&expansions=entities.mentions.username,attachments.media_keys`,
|
||||
{headers:{Authorization:`Bearer ${token}`}},
|
||||
)
|
||||
const medias = new Map(media.map(({media_key, type, url, preview_image_url}) => [media_key, (type === "photo") || (type === "animated_gif") ? url : type === "video" ? preview_image_url : null]))
|
||||
|
||||
//Limit tweets
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > keeping only ${limit} tweets`)
|
||||
tweets.splice(limit)
|
||||
}
|
||||
//Limit tweets
|
||||
if (limit > 0) {
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > keeping only ${limit} tweets`)
|
||||
tweets.splice(limit)
|
||||
}
|
||||
|
||||
//Format tweets
|
||||
await Promise.all(tweets.map(async tweet => {
|
||||
//Mentions and urls
|
||||
tweet.mentions = tweet.entities?.mentions?.map(({username}) => username) ?? []
|
||||
tweet.urls = new Map(tweet.entities?.urls?.map(({url, display_url:link}) => [url, link]) ?? [])
|
||||
//Attachments
|
||||
if (attachments) {
|
||||
//Retrieve linked content
|
||||
let linked = null
|
||||
if (tweet.urls.size) {
|
||||
linked = [...tweet.urls.keys()][tweet.urls.size - 1]
|
||||
tweet.text = tweet.text.replace(new RegExp(`(?:${linked})$`), "")
|
||||
}
|
||||
//Medias
|
||||
if (tweet.attachments)
|
||||
tweet.attachments = await Promise.all(tweet.attachments.media_keys.filter(key => medias.get(key)).map(key => medias.get(key)).map(async url => ({image:await imports.imgb64(url, {height:-1, width:450})})))
|
||||
if (linked) {
|
||||
const {result:{ogImage, ogSiteName:website, ogTitle:title, ogDescription:description}} = await imports.opengraph({url:linked})
|
||||
const image = await imports.imgb64(ogImage?.url, {height:-1, width:450, fallback:false})
|
||||
if (image) {
|
||||
if (tweet.attachments)
|
||||
tweet.attachments.unshift([{image, title, description, website}])
|
||||
else
|
||||
tweet.attachments = [{image, title, description, website}]
|
||||
}
|
||||
else
|
||||
tweet.text = `${tweet.text}\n${linked}`
|
||||
|
||||
//Format tweets
|
||||
await Promise.all(tweets.map(async tweet => {
|
||||
//Mentions and urls
|
||||
tweet.mentions = tweet.entities?.mentions?.map(({username}) => username) ?? []
|
||||
tweet.urls = new Map(tweet.entities?.urls?.map(({url, display_url:link}) => [url, link]) ?? [])
|
||||
//Attachments
|
||||
if (attachments) {
|
||||
//Retrieve linked content
|
||||
let linked = null
|
||||
if (tweet.urls.size) {
|
||||
linked = [...tweet.urls.keys()][tweet.urls.size-1]
|
||||
tweet.text = tweet.text.replace(new RegExp(`(?:${linked})$`), "")
|
||||
}
|
||||
//Medias
|
||||
if (tweet.attachments)
|
||||
tweet.attachments = await Promise.all(tweet.attachments.media_keys.filter(key => medias.get(key)).map(key => medias.get(key)).map(async url => ({image:await imports.imgb64(url, {height:-1, width:450})})))
|
||||
if (linked) {
|
||||
const {result:{ogImage, ogSiteName:website, ogTitle:title, ogDescription:description}} = await imports.opengraph({url:linked})
|
||||
const image = await imports.imgb64(ogImage?.url, {height:-1, width:450, fallback:false})
|
||||
if (image) {
|
||||
if (tweet.attachments)
|
||||
tweet.attachments.unshift([{image, title, description, website}])
|
||||
else
|
||||
tweet.attachments = [{image, title, description, website}]
|
||||
}
|
||||
else
|
||||
tweet.text = `${tweet.text}\n${linked}`
|
||||
}
|
||||
}
|
||||
else
|
||||
tweet.attachments = null
|
||||
//Format text
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > formatting tweet ${tweet.id}`)
|
||||
tweet.createdAt = `${imports.date(tweet.created_at, {timeStyle:"short", timeZone:data.config.timezone?.name})} on ${imports.date(tweet.created_at, {dateStyle:"short", timeZone:data.config.timezone?.name})}`
|
||||
tweet.text = imports.htmlescape(
|
||||
//Escape tags
|
||||
imports.htmlescape(tweet.text, {"<":true, ">":true})
|
||||
//Mentions
|
||||
.replace(new RegExp(`@(${tweet.mentions.join("|")})`, "gi"), '<span class="mention">@$1</span>')
|
||||
//Hashtags (this regex comes from the twitter source code)
|
||||
.replace(/(?<!&)[#|#]([a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f]*[a-z_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f][a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f]*)/gi, ' <span class="hashtag">#$1</span> ')
|
||||
//Line breaks
|
||||
.replace(/\n/g, "<br/>")
|
||||
//Links
|
||||
.replace(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})
|
||||
}))
|
||||
|
||||
//Result
|
||||
return {username, profile, list:tweets}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.errors?.[0]?.message ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
else
|
||||
tweet.attachments = null
|
||||
|
||||
|
||||
//Format text
|
||||
console.debug(`metrics/compute/${login}/plugins > tweets > formatting tweet ${tweet.id}`)
|
||||
tweet.createdAt = `${imports.date(tweet.created_at, {timeStyle:"short", timeZone:data.config.timezone?.name})} on ${imports.date(tweet.created_at, {dateStyle:"short", timeZone:data.config.timezone?.name})}`
|
||||
tweet.text = imports.htmlescape(
|
||||
//Escape tags
|
||||
imports.htmlescape(tweet.text, {"<":true, ">":true})
|
||||
//Mentions
|
||||
.replace(new RegExp(`@(${tweet.mentions.join("|")})`, "gi"), '<span class="mention">@$1</span>')
|
||||
//Hashtags (this regex comes from the twitter source code)
|
||||
.replace(
|
||||
/(?<!&)[#|#]([a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f]*[a-z_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f][a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f]*)/gi,
|
||||
' <span class="hashtag">#$1</span> ',
|
||||
)
|
||||
//Line breaks
|
||||
.replace(/\n/g, "<br/>")
|
||||
//Links
|
||||
.replace(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},
|
||||
)
|
||||
}))
|
||||
|
||||
//Result
|
||||
return {username, profile, list:tweets}
|
||||
}
|
||||
//Handle errors
|
||||
catch (error) {
|
||||
let message = "An error occured"
|
||||
if (error.isAxiosError) {
|
||||
const status = error.response?.status
|
||||
const description = error.response?.data?.errors?.[0]?.message ?? null
|
||||
message = `API returned ${status}${description ? ` (${description})` : ""}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//Setup
|
||||
export default async function ({ login, q, imports, data, account }, { enabled = false, token } = {}) {
|
||||
export default async function({login, q, imports, data, account}, {enabled = false, token} = {}) {
|
||||
//Plugin execution
|
||||
try {
|
||||
//Check if plugin is enabled and requirements are met
|
||||
@@ -7,30 +7,30 @@ export default async function ({ login, q, imports, data, account }, { enabled =
|
||||
return null
|
||||
|
||||
//Load inputs
|
||||
let { sections, days, limit, url, user } = imports.metadata.plugins.wakatime.inputs({ data, account, q })
|
||||
if (!limit) limit = void limit
|
||||
const range =
|
||||
{
|
||||
"7": "last_7_days",
|
||||
"30": "last_30_days",
|
||||
"180": "last_6_months",
|
||||
"365": "last_year",
|
||||
}[days] ?? "last_7_days"
|
||||
let {sections, days, limit, url, user} = imports.metadata.plugins.wakatime.inputs({data, account, q})
|
||||
if (!limit)
|
||||
limit = void limit
|
||||
const range = {
|
||||
"7":"last_7_days",
|
||||
"30":"last_30_days",
|
||||
"180":"last_6_months",
|
||||
"365":"last_year",
|
||||
}[days] ?? "last_7_days"
|
||||
|
||||
//Querying api and format result (https://wakatime.com/developers#stats)
|
||||
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 = {
|
||||
sections,
|
||||
days,
|
||||
time: {
|
||||
total: stats.total_seconds / (60 * 60),
|
||||
daily: stats.daily_average / (60 * 60),
|
||||
time:{
|
||||
total:stats.total_seconds / (60 * 60),
|
||||
daily: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),
|
||||
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),
|
||||
editors:stats.editors.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),
|
||||
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),
|
||||
}
|
||||
|
||||
//Result
|
||||
@@ -44,6 +44,6 @@ export default async function ({ login, q, imports, data, account }, { enabled =
|
||||
message = `API returned ${status}`
|
||||
error = error.response?.data ?? null
|
||||
}
|
||||
throw {error:{message, instance:error }}
|
||||
throw {error:{message, instance:error}}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**Template processor */
|
||||
export default async function(_, __, {imports}) {
|
||||
//Core
|
||||
await imports.plugins.core(...arguments)
|
||||
}
|
||||
export default async function(_, __, {imports}) {
|
||||
//Core
|
||||
await imports.plugins.core(...arguments)
|
||||
}
|
||||
|
||||
@@ -1,53 +1,53 @@
|
||||
/**Template processor */
|
||||
export default async function(_, {data}, {imports}) {
|
||||
//Core
|
||||
await imports.plugins.core(...arguments)
|
||||
await imports.plugins.core(...arguments)
|
||||
//Aliases
|
||||
const {user, computed, plugins} = data
|
||||
Object.assign(data, {
|
||||
//Base
|
||||
NAME:user.name,
|
||||
LOGIN:user.login,
|
||||
REGISTRATION_DATE:user.createdAt,
|
||||
REGISTERED_YEARS:computed.registered.years,
|
||||
LOCATION:user.location,
|
||||
WEBSITE:user.websiteUrl,
|
||||
REPOSITORIES:user.repositories.totalCount,
|
||||
REPOSITORIES_DISK_USAGE:user.repositories.totalDiskUsage,
|
||||
PACKAGES:user.packages.totalCount,
|
||||
STARRED:user.starredRepositories.totalCount,
|
||||
WATCHING:user.watching.totalCount,
|
||||
SPONSORING:user.sponsorshipsAsSponsor.totalCount,
|
||||
SPONSORS:user.sponsorshipsAsMaintainer.totalCount,
|
||||
REPOSITORIES_CONTRIBUTED_TO:user.repositoriesContributedTo.totalCount,
|
||||
COMMITS:computed.commits,
|
||||
COMMITS_PUBLIC:user.contributionsCollection.totalCommitContributions,
|
||||
COMMITS_PRIVATE:user.contributionsCollection.restrictedContributionsCount,
|
||||
ISSUES:user.contributionsCollection.totalIssueContributions,
|
||||
PULL_REQUESTS:user.contributionsCollection.totalPullRequestContributions,
|
||||
PULL_REQUESTS_REVIEWS:user.contributionsCollection.totalPullRequestReviewContributions,
|
||||
FOLLOWERS:user.followers.totalCount,
|
||||
FOLLOWING:user.following.totalCount,
|
||||
ISSUE_COMMENTS:user.issueComments.totalCount,
|
||||
ORGANIZATIONS:user.organizations.totalCount,
|
||||
WATCHERS:computed.repositories.watchers,
|
||||
STARGAZERS:computed.repositories.stargazers,
|
||||
FORKS:computed.repositories.forks,
|
||||
RELEASES:computed.repositories.releases,
|
||||
VERSION:data.meta.version,
|
||||
//Lines
|
||||
LINES_ADDED:plugins.lines?.added ?? 0,
|
||||
LINES_DELETED:plugins.lines?.deleted ?? 0,
|
||||
//Gists
|
||||
GISTS:plugins.gists?.totalCount ?? 0,
|
||||
GISTS_STARGAZERS:plugins.gists?.stargazers ?? 0,
|
||||
//Languages
|
||||
LANGUAGES:plugins.languages?.favorites?.map(({name, value, size, color}) => ({name, value, size, color})) ?? [],
|
||||
//Posts
|
||||
POSTS:plugins.posts?.list ?? [],
|
||||
//Tweets
|
||||
TWEETS:plugins.tweets?.list ?? [],
|
||||
//Topics
|
||||
TOPICS:plugins.topics?.list ?? [],
|
||||
})
|
||||
const {user, computed, plugins} = data
|
||||
Object.assign(data, {
|
||||
//Base
|
||||
NAME:user.name,
|
||||
LOGIN:user.login,
|
||||
REGISTRATION_DATE:user.createdAt,
|
||||
REGISTERED_YEARS:computed.registered.years,
|
||||
LOCATION:user.location,
|
||||
WEBSITE:user.websiteUrl,
|
||||
REPOSITORIES:user.repositories.totalCount,
|
||||
REPOSITORIES_DISK_USAGE:user.repositories.totalDiskUsage,
|
||||
PACKAGES:user.packages.totalCount,
|
||||
STARRED:user.starredRepositories.totalCount,
|
||||
WATCHING:user.watching.totalCount,
|
||||
SPONSORING:user.sponsorshipsAsSponsor.totalCount,
|
||||
SPONSORS:user.sponsorshipsAsMaintainer.totalCount,
|
||||
REPOSITORIES_CONTRIBUTED_TO:user.repositoriesContributedTo.totalCount,
|
||||
COMMITS:computed.commits,
|
||||
COMMITS_PUBLIC:user.contributionsCollection.totalCommitContributions,
|
||||
COMMITS_PRIVATE:user.contributionsCollection.restrictedContributionsCount,
|
||||
ISSUES:user.contributionsCollection.totalIssueContributions,
|
||||
PULL_REQUESTS:user.contributionsCollection.totalPullRequestContributions,
|
||||
PULL_REQUESTS_REVIEWS:user.contributionsCollection.totalPullRequestReviewContributions,
|
||||
FOLLOWERS:user.followers.totalCount,
|
||||
FOLLOWING:user.following.totalCount,
|
||||
ISSUE_COMMENTS:user.issueComments.totalCount,
|
||||
ORGANIZATIONS:user.organizations.totalCount,
|
||||
WATCHERS:computed.repositories.watchers,
|
||||
STARGAZERS:computed.repositories.stargazers,
|
||||
FORKS:computed.repositories.forks,
|
||||
RELEASES:computed.repositories.releases,
|
||||
VERSION:data.meta.version,
|
||||
//Lines
|
||||
LINES_ADDED:plugins.lines?.added ?? 0,
|
||||
LINES_DELETED:plugins.lines?.deleted ?? 0,
|
||||
//Gists
|
||||
GISTS:plugins.gists?.totalCount ?? 0,
|
||||
GISTS_STARGAZERS:plugins.gists?.stargazers ?? 0,
|
||||
//Languages
|
||||
LANGUAGES:plugins.languages?.favorites?.map(({name, value, size, color}) => ({name, value, size, color})) ?? [],
|
||||
//Posts
|
||||
POSTS:plugins.posts?.list ?? [],
|
||||
//Tweets
|
||||
TWEETS:plugins.tweets?.list ?? [],
|
||||
//Topics
|
||||
TOPICS:plugins.topics?.list ?? [],
|
||||
})
|
||||
}
|
||||
@@ -1,76 +1,76 @@
|
||||
/**Template processor */
|
||||
export default async function({login, q}, {data, rest, graphql, queries, account}, {pending, imports}) {
|
||||
//Check arguments
|
||||
const {repo} = q
|
||||
if (!repo) {
|
||||
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"}})
|
||||
return imports.plugins.core(...arguments)
|
||||
}
|
||||
console.debug(`metrics/compute/${login}/${repo} > switching to mode ${account}`)
|
||||
|
||||
//Retrieving single repository
|
||||
console.debug(`metrics/compute/${login}/${repo} > retrieving single repository ${repo}`)
|
||||
const {[account === "bypass" ? "user" : account]:{repository}} = await graphql(queries.base.repository({login, repo, account}))
|
||||
data.user.repositories.nodes = [repository]
|
||||
data.repo = repository
|
||||
|
||||
//Contributors and sponsors
|
||||
data.repo.contributors = {totalCount:(await rest.repos.listContributors({owner:data.repo.owner.login, repo})).data.length}
|
||||
data.repo.sponsorshipsAsMaintainer = data.user.sponsorshipsAsMaintainer
|
||||
|
||||
//Get commit activity
|
||||
console.debug(`metrics/compute/${login}/${repo} > querying api for commits`)
|
||||
const commits = []
|
||||
for (let page = 1; page < 100; page++) {
|
||||
console.debug(`metrics/compute/${login}/${repo} > loading page ${page}`)
|
||||
try {
|
||||
const {data} = await rest.repos.listCommits({owner:login, repo, per_page:100, page})
|
||||
if (!data.length) {
|
||||
console.debug(`metrics/compute/${login}/${repo} > no more page to load`)
|
||||
break
|
||||
}
|
||||
commits.push(...data)
|
||||
}
|
||||
catch (error) {
|
||||
if (/Git Repository is empty/.test(error))
|
||||
break
|
||||
throw error
|
||||
}
|
||||
}
|
||||
console.debug(`metrics/compute/${login}/${repo} > ${commits.length} commits loaded`)
|
||||
|
||||
//Override creation date, disk usage and website url
|
||||
data.user.createdAt = repository.createdAt
|
||||
data.user.repositories.totalDiskUsage = repository.diskUsage
|
||||
data.user.websiteUrl = repository.homepageUrl
|
||||
|
||||
//Override contributions calendar
|
||||
const days = 14
|
||||
//Compute relative date for each contribution
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
const contributions = commits.map(({commit}) => Math.abs(Math.ceil((now - new Date(commit.committer.date))/(24*60*60*1000))))
|
||||
//Count contributions per relative day
|
||||
const calendar = new Array(days).fill(0)
|
||||
for (const day of contributions)
|
||||
calendar[day]++
|
||||
calendar.splice(days)
|
||||
const max = Math.max(...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)"}}))
|
||||
|
||||
//Override plugins parameters
|
||||
q["projects.limit"] = 0
|
||||
|
||||
//Core
|
||||
await imports.plugins.core(...arguments)
|
||||
await Promise.all(pending)
|
||||
|
||||
//Set repository name
|
||||
data.user.name = `${data.user.login}/${repo}`
|
||||
|
||||
//Reformat projects names
|
||||
if (data.plugins.projects)
|
||||
data.plugins.projects.list?.map(project => project.name = project.name.replace(`(${login}/${repo})`, "").trim())
|
||||
export default async function({login, q}, {data, rest, graphql, queries, account}, {pending, imports}) {
|
||||
//Check arguments
|
||||
const {repo} = q
|
||||
if (!repo) {
|
||||
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'}})
|
||||
return imports.plugins.core(...arguments)
|
||||
}
|
||||
console.debug(`metrics/compute/${login}/${repo} > switching to mode ${account}`)
|
||||
|
||||
//Retrieving single repository
|
||||
console.debug(`metrics/compute/${login}/${repo} > retrieving single repository ${repo}`)
|
||||
const {[account === "bypass" ? "user" : account]:{repository}} = await graphql(queries.base.repository({login, repo, account}))
|
||||
data.user.repositories.nodes = [repository]
|
||||
data.repo = repository
|
||||
|
||||
//Contributors and sponsors
|
||||
data.repo.contributors = {totalCount:(await rest.repos.listContributors({owner:data.repo.owner.login, repo})).data.length}
|
||||
data.repo.sponsorshipsAsMaintainer = data.user.sponsorshipsAsMaintainer
|
||||
|
||||
//Get commit activity
|
||||
console.debug(`metrics/compute/${login}/${repo} > querying api for commits`)
|
||||
const commits = []
|
||||
for (let page = 1; page < 100; page++) {
|
||||
console.debug(`metrics/compute/${login}/${repo} > loading page ${page}`)
|
||||
try {
|
||||
const {data} = await rest.repos.listCommits({owner:login, repo, per_page:100, page})
|
||||
if (!data.length) {
|
||||
console.debug(`metrics/compute/${login}/${repo} > no more page to load`)
|
||||
break
|
||||
}
|
||||
commits.push(...data)
|
||||
}
|
||||
catch (error) {
|
||||
if (/Git Repository is empty/.test(error))
|
||||
break
|
||||
throw error
|
||||
}
|
||||
}
|
||||
console.debug(`metrics/compute/${login}/${repo} > ${commits.length} commits loaded`)
|
||||
|
||||
//Override creation date, disk usage and website url
|
||||
data.user.createdAt = repository.createdAt
|
||||
data.user.repositories.totalDiskUsage = repository.diskUsage
|
||||
data.user.websiteUrl = repository.homepageUrl
|
||||
|
||||
//Override contributions calendar
|
||||
const days = 14
|
||||
//Compute relative date for each contribution
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
const contributions = commits.map(({commit}) => Math.abs(Math.ceil((now - new Date(commit.committer.date)) / (24 * 60 * 60 * 1000))))
|
||||
//Count contributions per relative day
|
||||
const calendar = new Array(days).fill(0)
|
||||
for (const day of contributions)
|
||||
calendar[day]++
|
||||
calendar.splice(days)
|
||||
const max = Math.max(...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)"}}))
|
||||
|
||||
//Override plugins parameters
|
||||
q["projects.limit"] = 0
|
||||
|
||||
//Core
|
||||
await imports.plugins.core(...arguments)
|
||||
await Promise.all(pending)
|
||||
|
||||
//Set repository name
|
||||
data.user.name = `${data.user.login}/${repo}`
|
||||
|
||||
//Reformat projects names
|
||||
if (data.plugins.projects)
|
||||
data.plugins.projects.list?.map(project => project.name = project.name.replace(`(${login}/${repo})`, "").trim())
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**Template processor */
|
||||
export default async function({q}, _, {imports}) {
|
||||
//Core
|
||||
await imports.plugins.core(...arguments)
|
||||
//Disable optimization to keep white-spaces
|
||||
q.raw = true
|
||||
}
|
||||
export default async function({q}, _, {imports}) {
|
||||
//Core
|
||||
await imports.plugins.core(...arguments)
|
||||
//Disable optimization to keep white-spaces
|
||||
q.raw = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user