diff --git a/source/app/action/index.mjs b/source/app/action/index.mjs index f54fcbaf..bfc07a0c 100644 --- a/source/app/action/index.mjs +++ b/source/app/action/index.mjs @@ -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(/(?
[\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(/(?
[\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-(?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-(?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 = /(?)/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 = /(?)/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, ``) - 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, ``) + 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 + } - })() \ No newline at end of file + 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) + } +})() diff --git a/source/app/metrics/index.mjs b/source/app/metrics/index.mjs index 2fe4f365..446e1e68 100644 --- a/source/app/metrics/index.mjs +++ b/source/app/metrics/index.mjs @@ -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 "

⚠️ Failed to execute embed function: invalid arguments

" - } - //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 `` - } - //Rendering template source - let rendered = source.replace(/\{\{ (?[\s\S]*?) \}\}/g, "{%= $ %}") - 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 "

⚠️ Failed to execute embed function: invalid arguments

" + } + //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 `` + } + //Rendering template source + let rendered = source.replace(/\{\{ (?[\s\S]*?) \}\}/g, "{%= $ %}") + 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 + } +} diff --git a/source/app/metrics/metadata.mjs b/source/app/metrics/metadata.mjs index bc761b6d..2931ea4b 100644 --- a/source/app/metrics/metadata.mjs +++ b/source/app/metrics/metadata.mjs @@ -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*(?${Object.keys(inputs).join("|")}):`, "m"))?.groups?.input ?? null - if (input) - comments[input] = x.match(new RegExp(`(?[\\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(/(?[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? "" - - //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*(?${Object.keys(inputs).join("|")}):`, "m"))?.groups?.input ?? null + if (input) + comments[input] = x.match(new RegExp(`(?[\\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(/(?^[\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(/^### (?[\s\S]+?)\n/)?.groups?.name?.trim(), - index:meta.index ?? null, - formats:meta.formats ?? null, - supports:meta.supports ?? null, - readme:{ - demo:readme.match(/(?
[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? '' : ""), - 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(/(?
See documentation 🌍
[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? "" + + //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(/(?^[\s\S]+(?=[.]ejs$))/)?.groups?.plugin ?? null + if (plugin in compatibility) + compatibility[plugin] = true + } + } + + //Result + return { + name:meta.name ?? readme.match(/^### (?[\s\S]+?)\n/)?.groups?.name?.trim(), + index:meta.index ?? null, + formats:meta.formats ?? null, + supports:meta.supports ?? null, + readme:{ + demo:readme.match(/(?
[\s\S]*?<[/]table>)/)?.groups?.demo?.replace(/<[/]?(?:table|tr)>/g, "")?.trim() ?? (name === "community" ? '' : ""), + 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 + }, +} diff --git a/source/app/metrics/setup.mjs b/source/app/metrics/setup.mjs index 38966844..528e893c 100644 --- a/source/app/metrics/setup.mjs +++ b/source/app/metrics/setup.mjs @@ -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(/^(?[\s\S]+?)@(?[\s\S]+?):(?[\s\S]+?)(?[+]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(/^(?[\s\S]+?)@(?[\s\S]+?):(?[\s\S]+?)(?[+]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} +} diff --git a/source/app/metrics/utils.mjs b/source/app/metrics/utils.mjs index b5c652ab..a673fa96 100644 --- a/source/app/metrics/utils.mjs +++ b/source/app/metrics/utils.mjs @@ -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(/(?<=[.])(?[1-9]*)0+$/, "$") - .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["'"] ? "'" : "'") - } +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["'"] ? "'" : "'") +} /**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 = `` - return (await nodechartist(...arguments)) - .replace(/class="ct-chart-line">/, `class="ct-chart-line">${css}`) - } +export async function chartist() { + const css = `` + 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(/(?)(?[\s\S]*?)(?<\/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(${lines.length-codelines} more ${lines.length-codelines === 1 ? "line was" : "lines were"} trimmed)` : ""}${close}` - }) - return rendered + ) + break + } + default: + break } + //Trim code snippets + rendered = rendered.replace(/(?)(?[\s\S]*?)(?<\/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(${lines.length - codelines} more ${lines.length - codelines === 1 ? "line was" : "lines were"} trimmed)` : ""}${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(`
${rendered}
`, {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(`
${rendered}
`, {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(/^[^>]*)>${emoji}`, "g"), twemoji.replace(/()/, "$1 $ $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, ``) - } - } - 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(/^[^>]*)>${emoji}`, "g"), twemoji.replace(/()/, "$1 $ $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, ``) + } + } + 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}` - } \ No newline at end of file +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}` +} diff --git a/source/app/mocks/api/axios/get/lastfm.mjs b/source/app/mocks/api/axios/get/lastfm.mjs index da554197..71ddcee8 100644 --- a/source/app/mocks/api/axios/get/lastfm.mjs +++ b/source/app/mocks/api/axios/get/lastfm.mjs @@ -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:"", }, - }) - } - } + ], + }, + }, + }) + } } +} diff --git a/source/app/mocks/api/axios/get/nightscout.mjs b/source/app/mocks/api/axios/get/nightscout.mjs index 68d22d27..859f5684 100644 --- a/source/app/mocks/api/axios/get/nightscout.mjs +++ b/source/app/mocks/api/axios/get/nightscout.mjs @@ -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, - })), - }) - } - } \ No newline at end of file + //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, + })), + }) + } +} diff --git a/source/app/mocks/api/axios/get/pagespeed.mjs b/source/app/mocks/api/axios/get/pagespeed.mjs index feabf2cb..10f0fc9c 100644 --- a/source/app/mocks/api/axios/get/pagespeed.mjs +++ b/source/app/mocks/api/axios/get/pagespeed.mjs @@ -1,105 +1,105 @@ /**Mocked data */ - export default function({faker, url, options, login = faker.internet.userName()}) { - //Tested url - const tested = url.match(/&url=(?.*?)(?:&|$)/)?.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=(?.*?)(?:&|$)/)?.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()}`, + }, + }) + } } +} diff --git a/source/app/mocks/api/axios/get/spotify.mjs b/source/app/mocks/api/axios/get/spotify.mjs index 58b2535a..1ef898fb 100644 --- a/source/app/mocks/api/axios/get/spotify.mjs +++ b/source/app/mocks/api/axios/get/spotify.mjs @@ -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", + }, + }, + ], + }, + }) + } } +} diff --git a/source/app/mocks/api/axios/get/stackoverflow.mjs b/source/app/mocks/api/axios/get/stackoverflow.mjs index 02361850..3ec561d8 100644 --- a/source/app/mocks/api/axios/get/stackoverflow.mjs +++ b/source/app/mocks/api/axios/get/stackoverflow.mjs @@ -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[/](?\d+)/)?.groups?.id ?? NaN - const pagesize = Number(url.match(/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[/](?\d+)/)?.groups?.id ?? NaN + const pagesize = Number(url.match(/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), + }, + }) + } } +} diff --git a/source/app/mocks/api/axios/get/twitter.mjs b/source/app/mocks/api/axios/get/twitter.mjs index fe28b77e..a75761e1 100644 --- a/source/app/mocks/api/axios/get/twitter.mjs +++ b/source/app/mocks/api/axios/get/twitter.mjs @@ -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[/](?.*?)[?]/)?.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[/](?.*?)[?]/)?.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", + }, + }, + }) + } } +} diff --git a/source/app/mocks/api/axios/get/wakatime.mjs b/source/app/mocks/api/axios/get/wakatime.mjs index fc3a092b..2aef57c4 100644 --- a/source/app/mocks/api/axios/get/wakatime.mjs +++ b/source/app/mocks/api/axios/get/wakatime.mjs @@ -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), + }, + }, + }) + } } +} diff --git a/source/app/mocks/api/axios/get/yahoo.mjs b/source/app/mocks/api/axios/get/yahoo.mjs index 6108b227..b4b6dfe0 100644 --- a/source/app/mocks/api/axios/get/yahoo.mjs +++ b/source/app/mocks/api/axios/get/yahoo.mjs @@ -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:[], }, ], }, }, - }) - } - } - } \ No newline at end of file + ], + }, + }, + }) + } + } +} diff --git a/source/app/mocks/api/axios/post/anilist.mjs b/source/app/mocks/api/axios/post/anilist.mjs index ed97f36b..c36b3315 100644 --- a/source/app/mocks/api/axios/post/anilist.mjs +++ b/source/app/mocks/api/axios/post/anilist.mjs @@ -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}), + })), }, - }, + ], }, - }) - } - + }, + }, + }) } } +} diff --git a/source/app/mocks/api/axios/post/hashnode.mjs b/source/app/mocks/api/axios/post/hashnode.mjs index 9936df43..53e3a68f 100644 --- a/source/app/mocks/api/axios/post/hashnode.mjs +++ b/source/app/mocks/api/axios/post/hashnode.mjs @@ -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(), + })), }, }, - }) - } + }, + }, + }) } +} diff --git a/source/app/mocks/api/axios/post/spotify.mjs b/source/app/mocks/api/axios/post/spotify.mjs index c532b895..3b07b784 100644 --- a/source/app/mocks/api/axios/post/spotify.mjs +++ b/source/app/mocks/api/axios/post/spotify.mjs @@ -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", + }, + }) } } +} diff --git a/source/app/mocks/api/github/graphql/achievements.default.mjs b/source/app/mocks/api/github/graphql/achievements.default.mjs index ce14033e..4f0f8bd5 100644 --- a/source/app/mocks/api/github/graphql/achievements.default.mjs +++ b/source/app/mocks/api/github/graphql/achievements.default.mjs @@ -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)}, + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/achievements.metrics.mjs b/source/app/mocks/api/github/graphql/achievements.metrics.mjs index 104964ec..83334bc7 100644 --- a/source/app/mocks/api/github/graphql/achievements.metrics.mjs +++ b/source/app/mocks/api/github/graphql/achievements.metrics.mjs @@ -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}, + }) +} diff --git a/source/app/mocks/api/github/graphql/achievements.octocat.mjs b/source/app/mocks/api/github/graphql/achievements.octocat.mjs index 01da3e07..149ab7f0 100644 --- a/source/app/mocks/api/github/graphql/achievements.octocat.mjs +++ b/source/app/mocks/api/github/graphql/achievements.octocat.mjs @@ -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}, + }) +} diff --git a/source/app/mocks/api/github/graphql/achievements.organizations.mjs b/source/app/mocks/api/github/graphql/achievements.organizations.mjs index 4e8b10bc..b42ae751 100644 --- a/source/app/mocks/api/github/graphql/achievements.organizations.mjs +++ b/source/app/mocks/api/github/graphql/achievements.organizations.mjs @@ -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)}, + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/achievements.ranking.mjs b/source/app/mocks/api/github/graphql/achievements.ranking.mjs index b5ef92f8..6a5acef3 100644 --- a/source/app/mocks/api/github/graphql/achievements.ranking.mjs +++ b/source/app/mocks/api/github/graphql/achievements.ranking.mjs @@ -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)}, + }) +} diff --git a/source/app/mocks/api/github/graphql/base.repositories.mjs b/source/app/mocks/api/github/graphql/base.repositories.mjs index 0213f55f..2a9d6c8b 100644 --- a/source/app/mocks/api/github/graphql/base.repositories.mjs +++ b/source/app/mocks/api/github/graphql/base.repositories.mjs @@ -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 @@ }, }, }) - } +} diff --git a/source/app/mocks/api/github/graphql/base.repository.mjs b/source/app/mocks/api/github/graphql/base.repository.mjs index 293d0e0b..f5a3c776 100644 --- a/source/app/mocks/api/github/graphql/base.repository.mjs +++ b/source/app/mocks/api/github/graphql/base.repository.mjs @@ -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"}, }, - }) - } + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/base.user.mjs b/source/app/mocks/api/github/graphql/base.user.mjs index 72f0d679..bd1f762f 100644 --- a/source/app/mocks/api/github/graphql/base.user.mjs +++ b/source/app/mocks/api/github/graphql/base.user.mjs @@ -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)}, + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/contributors.commit.mjs b/source/app/mocks/api/github/graphql/contributors.commit.mjs index 02e6a115..8382f467 100644 --- a/source/app/mocks/api/github/graphql/contributors.commit.mjs +++ b/source/app/mocks/api/github/graphql/contributors.commit.mjs @@ -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(), }, - }) - } + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/followup.user.mjs b/source/app/mocks/api/github/graphql/followup.user.mjs index 4cc692c5..f9d1934d 100644 --- a/source/app/mocks/api/github/graphql/followup.user.mjs +++ b/source/app/mocks/api/github/graphql/followup.user.mjs @@ -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)}, + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/gists.default.mjs b/source/app/mocks/api/github/graphql/gists.default.mjs index 12ebf7f1..c56595cf 100644 --- a/source/app/mocks/api/github/graphql/gists.default.mjs +++ b/source/app/mocks/api/github/graphql/gists.default.mjs @@ -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 @@ }, }, }) - } +} diff --git a/source/app/mocks/api/github/graphql/introduction.organization.mjs b/source/app/mocks/api/github/graphql/introduction.organization.mjs index ba0dc1fe..1b6ebd66 100644 --- a/source/app/mocks/api/github/graphql/introduction.organization.mjs +++ b/source/app/mocks/api/github/graphql/introduction.organization.mjs @@ -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(), + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/introduction.repository.mjs b/source/app/mocks/api/github/graphql/introduction.repository.mjs index b9692649..0a3aba92 100644 --- a/source/app/mocks/api/github/graphql/introduction.repository.mjs +++ b/source/app/mocks/api/github/graphql/introduction.repository.mjs @@ -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(), + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/introduction.user.mjs b/source/app/mocks/api/github/graphql/introduction.user.mjs index 8dcb6741..782dde47 100644 --- a/source/app/mocks/api/github/graphql/introduction.user.mjs +++ b/source/app/mocks/api/github/graphql/introduction.user.mjs @@ -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(), + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/isocalendar.calendar.mjs b/source/app/mocks/api/github/graphql/isocalendar.calendar.mjs index 7e9896e1..45065a10 100644 --- a/source/app/mocks/api/github/graphql/isocalendar.calendar.mjs +++ b/source/app/mocks/api/github/graphql/isocalendar.calendar.mjs @@ -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: "(?\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: "(?\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: "(?\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: "(?\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, + }, + }, + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/licenses.default.mjs b/source/app/mocks/api/github/graphql/licenses.default.mjs index 5da8a885..67758a53 100644 --- a/source/app/mocks/api/github/graphql/licenses.default.mjs +++ b/source/app/mocks/api/github/graphql/licenses.default.mjs @@ -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"}, + ], + }, + ], + }) +} diff --git a/source/app/mocks/api/github/graphql/licenses.repository.mjs b/source/app/mocks/api/github/graphql/licenses.repository.mjs index 2eb533c2..ed494cd2 100644 --- a/source/app/mocks/api/github/graphql/licenses.repository.mjs +++ b/source/app/mocks/api/github/graphql/licenses.repository.mjs @@ -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, }, - }) - } + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/notable.contributions.mjs b/source/app/mocks/api/github/graphql/notable.contributions.mjs index 9c19d86c..70318e66 100644 --- a/source/app/mocks/api/github/graphql/notable.contributions.mjs +++ b/source/app/mocks/api/github/graphql/notable.contributions.mjs @@ -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 @@ }, }, }) - } +} diff --git a/source/app/mocks/api/github/graphql/people.default.mjs b/source/app/mocks/api/github/graphql/people.default.mjs index 1ea846a2..560d3aa2 100644 --- a/source/app/mocks/api/github/graphql/people.default.mjs +++ b/source/app/mocks/api/github/graphql/people.default.mjs @@ -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(/(?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(/(?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 @@ }, }, }) - } +} diff --git a/source/app/mocks/api/github/graphql/people.repository.mjs b/source/app/mocks/api/github/graphql/people.repository.mjs index 979b146c..a4994fdb 100644 --- a/source/app/mocks/api/github/graphql/people.repository.mjs +++ b/source/app/mocks/api/github/graphql/people.repository.mjs @@ -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(/(?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(/(?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 @@ }, }, }) - } +} diff --git a/source/app/mocks/api/github/graphql/people.sponsors.mjs b/source/app/mocks/api/github/graphql/people.sponsors.mjs index 3c015d77..f27fd8f6 100644 --- a/source/app/mocks/api/github/graphql/people.sponsors.mjs +++ b/source/app/mocks/api/github/graphql/people.sponsors.mjs @@ -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(/(?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(/(?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 @@ }, }, }) - } +} diff --git a/source/app/mocks/api/github/graphql/projects.repository.mjs b/source/app/mocks/api/github/graphql/projects.repository.mjs index 1ee652ff..d977cf2b 100644 --- a/source/app/mocks/api/github/graphql/projects.repository.mjs +++ b/source/app/mocks/api/github/graphql/projects.repository.mjs @@ -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, }, }, }, - }) - } + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/projects.user.mjs b/source/app/mocks/api/github/graphql/projects.user.mjs index 83be6de6..fa24db1b 100644 --- a/source/app/mocks/api/github/graphql/projects.user.mjs +++ b/source/app/mocks/api/github/graphql/projects.user.mjs @@ -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, }, - ], - }, + }, + ], }, - }) - } + }, + }) +} diff --git a/source/app/mocks/api/github/graphql/reactions.default.mjs b/source/app/mocks/api/github/graphql/reactions.default.mjs index 64432d55..2f2c2c21 100644 --- a/source/app/mocks/api/github/graphql/reactions.default.mjs +++ b/source/app/mocks/api/github/graphql/reactions.default.mjs @@ -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(/(?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(/(?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 @@ }, }, }) - } +} diff --git a/source/app/mocks/api/github/graphql/stargazers.default.mjs b/source/app/mocks/api/github/graphql/stargazers.default.mjs index ec0a440a..e5f2ac44 100644 --- a/source/app/mocks/api/github/graphql/stargazers.default.mjs +++ b/source/app/mocks/api/github/graphql/stargazers.default.mjs @@ -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 @@ }, }, }) - } +} diff --git a/source/app/mocks/api/github/graphql/stars.default.mjs b/source/app/mocks/api/github/graphql/stars.default.mjs index c62ff74a..675ae042 100644 --- a/source/app/mocks/api/github/graphql/stars.default.mjs +++ b/source/app/mocks/api/github/graphql/stars.default.mjs @@ -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", }, }, - ], - }, + }, + ], }, - }) - } + }, + }) +} diff --git a/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs b/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs index 98ead0fd..f0044356 100644 --- a/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs +++ b/source/app/mocks/api/github/rest/activity/listEventsForAuthenticatedUser.mjs @@ -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, + }, + ], + }) +} diff --git a/source/app/mocks/api/github/rest/activity/listRepoEvents.mjs b/source/app/mocks/api/github/rest/activity/listRepoEvents.mjs index 785b06f5..d328c1a5 100644 --- a/source/app/mocks/api/github/rest/activity/listRepoEvents.mjs +++ b/source/app/mocks/api/github/rest/activity/listRepoEvents.mjs @@ -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) +} diff --git a/source/app/mocks/api/github/rest/emojis/get.mjs b/source/app/mocks/api/github/rest/emojis/get.mjs index 970351ff..435eb042 100644 --- a/source/app/mocks/api/github/rest/emojis/get.mjs +++ b/source/app/mocks/api/github/rest/emojis/get.mjs @@ -1,1814 +1,1814 @@ /**Mocked data */ - export default function({faker}, target, that) { - console.debug("metrics/compute/mocks > mocking rest api result > rest.emojis.get") - return ({ - status:200, - url:"https://api.github.com/emojis", - headers:{ - server:"GitHub.com", - status:"200 OK", - "x-oauth-scopes":"repo", - }, - data:{ - "100":"https://github.githubassets.com/images/icons/emoji/unicode/1f4af.png?v8", - "1234":"https://github.githubassets.com/images/icons/emoji/unicode/1f522.png?v8", - "+1":"https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8", - "-1":"https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8", - "1st_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f947.png?v8", - "2nd_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f948.png?v8", - "3rd_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f949.png?v8", - "8ball":"https://github.githubassets.com/images/icons/emoji/unicode/1f3b1.png?v8", - a:"https://github.githubassets.com/images/icons/emoji/unicode/1f170.png?v8", - ab:"https://github.githubassets.com/images/icons/emoji/unicode/1f18e.png?v8", - abacus:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ee.png?v8", - abc:"https://github.githubassets.com/images/icons/emoji/unicode/1f524.png?v8", - abcd:"https://github.githubassets.com/images/icons/emoji/unicode/1f521.png?v8", - accept:"https://github.githubassets.com/images/icons/emoji/unicode/1f251.png?v8", - adhesive_bandage:"https://github.githubassets.com/images/icons/emoji/unicode/1fa79.png?v8", - adult:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1.png?v8", - aerial_tramway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a1.png?v8", - afghanistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1eb.png?v8", - airplane:"https://github.githubassets.com/images/icons/emoji/unicode/2708.png?v8", - aland_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fd.png?v8", - alarm_clock:"https://github.githubassets.com/images/icons/emoji/unicode/23f0.png?v8", - albania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f1.png?v8", - alembic:"https://github.githubassets.com/images/icons/emoji/unicode/2697.png?v8", - algeria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ff.png?v8", - alien:"https://github.githubassets.com/images/icons/emoji/unicode/1f47d.png?v8", - ambulance:"https://github.githubassets.com/images/icons/emoji/unicode/1f691.png?v8", - american_samoa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f8.png?v8", - amphora:"https://github.githubassets.com/images/icons/emoji/unicode/1f3fa.png?v8", - anchor:"https://github.githubassets.com/images/icons/emoji/unicode/2693.png?v8", - andorra:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1e9.png?v8", - angel:"https://github.githubassets.com/images/icons/emoji/unicode/1f47c.png?v8", - anger:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a2.png?v8", - angola:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f4.png?v8", - angry:"https://github.githubassets.com/images/icons/emoji/unicode/1f620.png?v8", - anguilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ee.png?v8", - anguished:"https://github.githubassets.com/images/icons/emoji/unicode/1f627.png?v8", - ant:"https://github.githubassets.com/images/icons/emoji/unicode/1f41c.png?v8", - antarctica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f6.png?v8", - antigua_barbuda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ec.png?v8", - apple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34e.png?v8", - aquarius:"https://github.githubassets.com/images/icons/emoji/unicode/2652.png?v8", - argentina:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f7.png?v8", - aries:"https://github.githubassets.com/images/icons/emoji/unicode/2648.png?v8", - armenia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f2.png?v8", - arrow_backward:"https://github.githubassets.com/images/icons/emoji/unicode/25c0.png?v8", - arrow_double_down:"https://github.githubassets.com/images/icons/emoji/unicode/23ec.png?v8", - arrow_double_up:"https://github.githubassets.com/images/icons/emoji/unicode/23eb.png?v8", - arrow_down:"https://github.githubassets.com/images/icons/emoji/unicode/2b07.png?v8", - arrow_down_small:"https://github.githubassets.com/images/icons/emoji/unicode/1f53d.png?v8", - arrow_forward:"https://github.githubassets.com/images/icons/emoji/unicode/25b6.png?v8", - arrow_heading_down:"https://github.githubassets.com/images/icons/emoji/unicode/2935.png?v8", - arrow_heading_up:"https://github.githubassets.com/images/icons/emoji/unicode/2934.png?v8", - arrow_left:"https://github.githubassets.com/images/icons/emoji/unicode/2b05.png?v8", - arrow_lower_left:"https://github.githubassets.com/images/icons/emoji/unicode/2199.png?v8", - arrow_lower_right:"https://github.githubassets.com/images/icons/emoji/unicode/2198.png?v8", - arrow_right:"https://github.githubassets.com/images/icons/emoji/unicode/27a1.png?v8", - arrow_right_hook:"https://github.githubassets.com/images/icons/emoji/unicode/21aa.png?v8", - arrow_up:"https://github.githubassets.com/images/icons/emoji/unicode/2b06.png?v8", - arrow_up_down:"https://github.githubassets.com/images/icons/emoji/unicode/2195.png?v8", - arrow_up_small:"https://github.githubassets.com/images/icons/emoji/unicode/1f53c.png?v8", - arrow_upper_left:"https://github.githubassets.com/images/icons/emoji/unicode/2196.png?v8", - arrow_upper_right:"https://github.githubassets.com/images/icons/emoji/unicode/2197.png?v8", - arrows_clockwise:"https://github.githubassets.com/images/icons/emoji/unicode/1f503.png?v8", - arrows_counterclockwise:"https://github.githubassets.com/images/icons/emoji/unicode/1f504.png?v8", - art:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a8.png?v8", - articulated_lorry:"https://github.githubassets.com/images/icons/emoji/unicode/1f69b.png?v8", - artificial_satellite:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f0.png?v8", - artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3a8.png?v8", - aruba:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fc.png?v8", - ascension_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1e8.png?v8", - asterisk:"https://github.githubassets.com/images/icons/emoji/unicode/002a-20e3.png?v8", - astonished:"https://github.githubassets.com/images/icons/emoji/unicode/1f632.png?v8", - astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f680.png?v8", - athletic_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45f.png?v8", - atm:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e7.png?v8", - atom:"https://github.githubassets.com/images/icons/emoji/atom.png?v8", - atom_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/269b.png?v8", - australia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fa.png?v8", - austria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f9.png?v8", - auto_rickshaw:"https://github.githubassets.com/images/icons/emoji/unicode/1f6fa.png?v8", - avocado:"https://github.githubassets.com/images/icons/emoji/unicode/1f951.png?v8", - axe:"https://github.githubassets.com/images/icons/emoji/unicode/1fa93.png?v8", - azerbaijan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ff.png?v8", - b:"https://github.githubassets.com/images/icons/emoji/unicode/1f171.png?v8", - baby:"https://github.githubassets.com/images/icons/emoji/unicode/1f476.png?v8", - baby_bottle:"https://github.githubassets.com/images/icons/emoji/unicode/1f37c.png?v8", - baby_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f424.png?v8", - baby_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bc.png?v8", - back:"https://github.githubassets.com/images/icons/emoji/unicode/1f519.png?v8", - bacon:"https://github.githubassets.com/images/icons/emoji/unicode/1f953.png?v8", - badger:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a1.png?v8", - badminton:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f8.png?v8", - bagel:"https://github.githubassets.com/images/icons/emoji/unicode/1f96f.png?v8", - baggage_claim:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c4.png?v8", - baguette_bread:"https://github.githubassets.com/images/icons/emoji/unicode/1f956.png?v8", - bahamas:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f8.png?v8", - bahrain:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ed.png?v8", - balance_scale:"https://github.githubassets.com/images/icons/emoji/unicode/2696.png?v8", - bald_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b2.png?v8", - bald_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b2.png?v8", - ballet_shoes:"https://github.githubassets.com/images/icons/emoji/unicode/1fa70.png?v8", - balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f388.png?v8", - ballot_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f5f3.png?v8", - ballot_box_with_check:"https://github.githubassets.com/images/icons/emoji/unicode/2611.png?v8", - bamboo:"https://github.githubassets.com/images/icons/emoji/unicode/1f38d.png?v8", - banana:"https://github.githubassets.com/images/icons/emoji/unicode/1f34c.png?v8", - bangbang:"https://github.githubassets.com/images/icons/emoji/unicode/203c.png?v8", - bangladesh:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e9.png?v8", - banjo:"https://github.githubassets.com/images/icons/emoji/unicode/1fa95.png?v8", - bank:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e6.png?v8", - bar_chart:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ca.png?v8", - barbados:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e7.png?v8", - barber:"https://github.githubassets.com/images/icons/emoji/unicode/1f488.png?v8", - baseball:"https://github.githubassets.com/images/icons/emoji/unicode/26be.png?v8", - basecamp:"https://github.githubassets.com/images/icons/emoji/basecamp.png?v8", - basecampy:"https://github.githubassets.com/images/icons/emoji/basecampy.png?v8", - basket:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fa.png?v8", - basketball:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c0.png?v8", - basketball_man:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2642.png?v8", - basketball_woman:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2640.png?v8", - bat:"https://github.githubassets.com/images/icons/emoji/unicode/1f987.png?v8", - bath:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c0.png?v8", - bathtub:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c1.png?v8", - battery:"https://github.githubassets.com/images/icons/emoji/unicode/1f50b.png?v8", - beach_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d6.png?v8", - bear:"https://github.githubassets.com/images/icons/emoji/unicode/1f43b.png?v8", - bearded_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d4.png?v8", - bed:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cf.png?v8", - bee:"https://github.githubassets.com/images/icons/emoji/unicode/1f41d.png?v8", - beer:"https://github.githubassets.com/images/icons/emoji/unicode/1f37a.png?v8", - beers:"https://github.githubassets.com/images/icons/emoji/unicode/1f37b.png?v8", - beetle:"https://github.githubassets.com/images/icons/emoji/unicode/1f41e.png?v8", - beginner:"https://github.githubassets.com/images/icons/emoji/unicode/1f530.png?v8", - belarus:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fe.png?v8", - belgium:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ea.png?v8", - belize:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ff.png?v8", - bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f514.png?v8", - bellhop_bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ce.png?v8", - benin:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ef.png?v8", - bento:"https://github.githubassets.com/images/icons/emoji/unicode/1f371.png?v8", - bermuda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f2.png?v8", - beverage_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c3.png?v8", - bhutan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f9.png?v8", - bicyclist:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4.png?v8", - bike:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b2.png?v8", - biking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4-2642.png?v8", - biking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4-2640.png?v8", - bikini:"https://github.githubassets.com/images/icons/emoji/unicode/1f459.png?v8", - billed_cap:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e2.png?v8", - biohazard:"https://github.githubassets.com/images/icons/emoji/unicode/2623.png?v8", - bird:"https://github.githubassets.com/images/icons/emoji/unicode/1f426.png?v8", - birthday:"https://github.githubassets.com/images/icons/emoji/unicode/1f382.png?v8", - black_circle:"https://github.githubassets.com/images/icons/emoji/unicode/26ab.png?v8", - black_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4.png?v8", - black_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a4.png?v8", - black_joker:"https://github.githubassets.com/images/icons/emoji/unicode/1f0cf.png?v8", - black_large_square:"https://github.githubassets.com/images/icons/emoji/unicode/2b1b.png?v8", - black_medium_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fe.png?v8", - black_medium_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fc.png?v8", - black_nib:"https://github.githubassets.com/images/icons/emoji/unicode/2712.png?v8", - black_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25aa.png?v8", - black_square_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f532.png?v8", - blond_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2642.png?v8", - blond_haired_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f471.png?v8", - blond_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2640.png?v8", - blonde_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2640.png?v8", - blossom:"https://github.githubassets.com/images/icons/emoji/unicode/1f33c.png?v8", - blowfish:"https://github.githubassets.com/images/icons/emoji/unicode/1f421.png?v8", - blue_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d8.png?v8", - blue_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f699.png?v8", - blue_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f499.png?v8", - blue_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e6.png?v8", - blush:"https://github.githubassets.com/images/icons/emoji/unicode/1f60a.png?v8", - boar:"https://github.githubassets.com/images/icons/emoji/unicode/1f417.png?v8", - boat:"https://github.githubassets.com/images/icons/emoji/unicode/26f5.png?v8", - bolivia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f4.png?v8", - bomb:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a3.png?v8", - bone:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b4.png?v8", - book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d6.png?v8", - bookmark:"https://github.githubassets.com/images/icons/emoji/unicode/1f516.png?v8", - bookmark_tabs:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d1.png?v8", - books:"https://github.githubassets.com/images/icons/emoji/unicode/1f4da.png?v8", - boom:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png?v8", - boot:"https://github.githubassets.com/images/icons/emoji/unicode/1f462.png?v8", - bosnia_herzegovina:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e6.png?v8", - botswana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fc.png?v8", - bouncing_ball_man:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2642.png?v8", - bouncing_ball_person:"https://github.githubassets.com/images/icons/emoji/unicode/26f9.png?v8", - bouncing_ball_woman:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2640.png?v8", - bouquet:"https://github.githubassets.com/images/icons/emoji/unicode/1f490.png?v8", - bouvet_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fb.png?v8", - bow:"https://github.githubassets.com/images/icons/emoji/unicode/1f647.png?v8", - bow_and_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f9.png?v8", - bowing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f647-2642.png?v8", - bowing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f647-2640.png?v8", - bowl_with_spoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f963.png?v8", - bowling:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b3.png?v8", - bowtie:"https://github.githubassets.com/images/icons/emoji/bowtie.png?v8", - boxing_glove:"https://github.githubassets.com/images/icons/emoji/unicode/1f94a.png?v8", - boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f466.png?v8", - brain:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e0.png?v8", - brazil:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f7.png?v8", - bread:"https://github.githubassets.com/images/icons/emoji/unicode/1f35e.png?v8", - breast_feeding:"https://github.githubassets.com/images/icons/emoji/unicode/1f931.png?v8", - bricks:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f1.png?v8", - bride_with_veil:"https://github.githubassets.com/images/icons/emoji/unicode/1f470.png?v8", - bridge_at_night:"https://github.githubassets.com/images/icons/emoji/unicode/1f309.png?v8", - briefcase:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bc.png?v8", - british_indian_ocean_territory:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f4.png?v8", - british_virgin_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ec.png?v8", - broccoli:"https://github.githubassets.com/images/icons/emoji/unicode/1f966.png?v8", - broken_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f494.png?v8", - broom:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f9.png?v8", - brown_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e4.png?v8", - brown_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f90e.png?v8", - brown_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7eb.png?v8", - brunei:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f3.png?v8", - bug:"https://github.githubassets.com/images/icons/emoji/unicode/1f41b.png?v8", - building_construction:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d7.png?v8", - bulb:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a1.png?v8", - bulgaria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ec.png?v8", - bullettrain_front:"https://github.githubassets.com/images/icons/emoji/unicode/1f685.png?v8", - bullettrain_side:"https://github.githubassets.com/images/icons/emoji/unicode/1f684.png?v8", - burkina_faso:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1eb.png?v8", - burrito:"https://github.githubassets.com/images/icons/emoji/unicode/1f32f.png?v8", - burundi:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ee.png?v8", - bus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68c.png?v8", - business_suit_levitating:"https://github.githubassets.com/images/icons/emoji/unicode/1f574.png?v8", - busstop:"https://github.githubassets.com/images/icons/emoji/unicode/1f68f.png?v8", - bust_in_silhouette:"https://github.githubassets.com/images/icons/emoji/unicode/1f464.png?v8", - busts_in_silhouette:"https://github.githubassets.com/images/icons/emoji/unicode/1f465.png?v8", - butter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c8.png?v8", - butterfly:"https://github.githubassets.com/images/icons/emoji/unicode/1f98b.png?v8", - cactus:"https://github.githubassets.com/images/icons/emoji/unicode/1f335.png?v8", - cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f370.png?v8", - calendar:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c6.png?v8", - call_me_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f919.png?v8", - calling:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f2.png?v8", - cambodia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ed.png?v8", - camel:"https://github.githubassets.com/images/icons/emoji/unicode/1f42b.png?v8", - camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f7.png?v8", - camera_flash:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f8.png?v8", - cameroon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f2.png?v8", - camping:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d5.png?v8", - canada:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e6.png?v8", - canary_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1e8.png?v8", - cancer:"https://github.githubassets.com/images/icons/emoji/unicode/264b.png?v8", - candle:"https://github.githubassets.com/images/icons/emoji/unicode/1f56f.png?v8", - candy:"https://github.githubassets.com/images/icons/emoji/unicode/1f36c.png?v8", - canned_food:"https://github.githubassets.com/images/icons/emoji/unicode/1f96b.png?v8", - canoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f6.png?v8", - cape_verde:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fb.png?v8", - capital_abcd:"https://github.githubassets.com/images/icons/emoji/unicode/1f520.png?v8", - capricorn:"https://github.githubassets.com/images/icons/emoji/unicode/2651.png?v8", - car:"https://github.githubassets.com/images/icons/emoji/unicode/1f697.png?v8", - card_file_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c3.png?v8", - card_index:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c7.png?v8", - card_index_dividers:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c2.png?v8", - caribbean_netherlands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f6.png?v8", - carousel_horse:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a0.png?v8", - carrot:"https://github.githubassets.com/images/icons/emoji/unicode/1f955.png?v8", - cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938.png?v8", - cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f431.png?v8", - cat2:"https://github.githubassets.com/images/icons/emoji/unicode/1f408.png?v8", - cayman_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1fe.png?v8", - cd:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bf.png?v8", - central_african_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1eb.png?v8", - ceuta_melilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1e6.png?v8", - chad:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e9.png?v8", - chains:"https://github.githubassets.com/images/icons/emoji/unicode/26d3.png?v8", - chair:"https://github.githubassets.com/images/icons/emoji/unicode/1fa91.png?v8", - champagne:"https://github.githubassets.com/images/icons/emoji/unicode/1f37e.png?v8", - chart:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b9.png?v8", - chart_with_downwards_trend:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c9.png?v8", - chart_with_upwards_trend:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c8.png?v8", - checkered_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c1.png?v8", - cheese:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c0.png?v8", - cherries:"https://github.githubassets.com/images/icons/emoji/unicode/1f352.png?v8", - cherry_blossom:"https://github.githubassets.com/images/icons/emoji/unicode/1f338.png?v8", - chess_pawn:"https://github.githubassets.com/images/icons/emoji/unicode/265f.png?v8", - chestnut:"https://github.githubassets.com/images/icons/emoji/unicode/1f330.png?v8", - chicken:"https://github.githubassets.com/images/icons/emoji/unicode/1f414.png?v8", - child:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d2.png?v8", - children_crossing:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b8.png?v8", - chile:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f1.png?v8", - chipmunk:"https://github.githubassets.com/images/icons/emoji/unicode/1f43f.png?v8", - chocolate_bar:"https://github.githubassets.com/images/icons/emoji/unicode/1f36b.png?v8", - chopsticks:"https://github.githubassets.com/images/icons/emoji/unicode/1f962.png?v8", - christmas_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fd.png?v8", - christmas_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f384.png?v8", - church:"https://github.githubassets.com/images/icons/emoji/unicode/26ea.png?v8", - cinema:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a6.png?v8", - circus_tent:"https://github.githubassets.com/images/icons/emoji/unicode/1f3aa.png?v8", - city_sunrise:"https://github.githubassets.com/images/icons/emoji/unicode/1f307.png?v8", - city_sunset:"https://github.githubassets.com/images/icons/emoji/unicode/1f306.png?v8", - cityscape:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d9.png?v8", - cl:"https://github.githubassets.com/images/icons/emoji/unicode/1f191.png?v8", - clamp:"https://github.githubassets.com/images/icons/emoji/unicode/1f5dc.png?v8", - clap:"https://github.githubassets.com/images/icons/emoji/unicode/1f44f.png?v8", - clapper:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ac.png?v8", - classical_building:"https://github.githubassets.com/images/icons/emoji/unicode/1f3db.png?v8", - climbing:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7.png?v8", - climbing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7-2642.png?v8", - climbing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7-2640.png?v8", - clinking_glasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f942.png?v8", - clipboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cb.png?v8", - clipperton_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f5.png?v8", - clock1:"https://github.githubassets.com/images/icons/emoji/unicode/1f550.png?v8", - clock10:"https://github.githubassets.com/images/icons/emoji/unicode/1f559.png?v8", - clock1030:"https://github.githubassets.com/images/icons/emoji/unicode/1f565.png?v8", - clock11:"https://github.githubassets.com/images/icons/emoji/unicode/1f55a.png?v8", - clock1130:"https://github.githubassets.com/images/icons/emoji/unicode/1f566.png?v8", - clock12:"https://github.githubassets.com/images/icons/emoji/unicode/1f55b.png?v8", - clock1230:"https://github.githubassets.com/images/icons/emoji/unicode/1f567.png?v8", - clock130:"https://github.githubassets.com/images/icons/emoji/unicode/1f55c.png?v8", - clock2:"https://github.githubassets.com/images/icons/emoji/unicode/1f551.png?v8", - clock230:"https://github.githubassets.com/images/icons/emoji/unicode/1f55d.png?v8", - clock3:"https://github.githubassets.com/images/icons/emoji/unicode/1f552.png?v8", - clock330:"https://github.githubassets.com/images/icons/emoji/unicode/1f55e.png?v8", - clock4:"https://github.githubassets.com/images/icons/emoji/unicode/1f553.png?v8", - clock430:"https://github.githubassets.com/images/icons/emoji/unicode/1f55f.png?v8", - clock5:"https://github.githubassets.com/images/icons/emoji/unicode/1f554.png?v8", - clock530:"https://github.githubassets.com/images/icons/emoji/unicode/1f560.png?v8", - clock6:"https://github.githubassets.com/images/icons/emoji/unicode/1f555.png?v8", - clock630:"https://github.githubassets.com/images/icons/emoji/unicode/1f561.png?v8", - clock7:"https://github.githubassets.com/images/icons/emoji/unicode/1f556.png?v8", - clock730:"https://github.githubassets.com/images/icons/emoji/unicode/1f562.png?v8", - clock8:"https://github.githubassets.com/images/icons/emoji/unicode/1f557.png?v8", - clock830:"https://github.githubassets.com/images/icons/emoji/unicode/1f563.png?v8", - clock9:"https://github.githubassets.com/images/icons/emoji/unicode/1f558.png?v8", - clock930:"https://github.githubassets.com/images/icons/emoji/unicode/1f564.png?v8", - closed_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d5.png?v8", - closed_lock_with_key:"https://github.githubassets.com/images/icons/emoji/unicode/1f510.png?v8", - closed_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/1f302.png?v8", - cloud:"https://github.githubassets.com/images/icons/emoji/unicode/2601.png?v8", - cloud_with_lightning:"https://github.githubassets.com/images/icons/emoji/unicode/1f329.png?v8", - cloud_with_lightning_and_rain:"https://github.githubassets.com/images/icons/emoji/unicode/26c8.png?v8", - cloud_with_rain:"https://github.githubassets.com/images/icons/emoji/unicode/1f327.png?v8", - cloud_with_snow:"https://github.githubassets.com/images/icons/emoji/unicode/1f328.png?v8", - clown_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f921.png?v8", - clubs:"https://github.githubassets.com/images/icons/emoji/unicode/2663.png?v8", - cn:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f3.png?v8", - coat:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e5.png?v8", - cocktail:"https://github.githubassets.com/images/icons/emoji/unicode/1f378.png?v8", - coconut:"https://github.githubassets.com/images/icons/emoji/unicode/1f965.png?v8", - cocos_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e8.png?v8", - coffee:"https://github.githubassets.com/images/icons/emoji/unicode/2615.png?v8", - coffin:"https://github.githubassets.com/images/icons/emoji/unicode/26b0.png?v8", - cold_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f976.png?v8", - cold_sweat:"https://github.githubassets.com/images/icons/emoji/unicode/1f630.png?v8", - collision:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png?v8", - colombia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f4.png?v8", - comet:"https://github.githubassets.com/images/icons/emoji/unicode/2604.png?v8", - comoros:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f2.png?v8", - compass:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ed.png?v8", - computer:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bb.png?v8", - computer_mouse:"https://github.githubassets.com/images/icons/emoji/unicode/1f5b1.png?v8", - confetti_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f38a.png?v8", - confounded:"https://github.githubassets.com/images/icons/emoji/unicode/1f616.png?v8", - confused:"https://github.githubassets.com/images/icons/emoji/unicode/1f615.png?v8", - congo_brazzaville:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ec.png?v8", - congo_kinshasa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e9.png?v8", - congratulations:"https://github.githubassets.com/images/icons/emoji/unicode/3297.png?v8", - construction:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a7.png?v8", - construction_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f477.png?v8", - construction_worker_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f477-2642.png?v8", - construction_worker_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f477-2640.png?v8", - control_knobs:"https://github.githubassets.com/images/icons/emoji/unicode/1f39b.png?v8", - convenience_store:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ea.png?v8", - cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f373.png?v8", - cook_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f0.png?v8", - cookie:"https://github.githubassets.com/images/icons/emoji/unicode/1f36a.png?v8", - cool:"https://github.githubassets.com/images/icons/emoji/unicode/1f192.png?v8", - cop:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e.png?v8", - copyright:"https://github.githubassets.com/images/icons/emoji/unicode/00a9.png?v8", - corn:"https://github.githubassets.com/images/icons/emoji/unicode/1f33d.png?v8", - costa_rica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f7.png?v8", - cote_divoire:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ee.png?v8", - couch_and_lamp:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cb.png?v8", - couple:"https://github.githubassets.com/images/icons/emoji/unicode/1f46b.png?v8", - couple_with_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f491.png?v8", - couple_with_heart_man_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2764-1f468.png?v8", - couple_with_heart_woman_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f468.png?v8", - couple_with_heart_woman_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f469.png?v8", - couplekiss:"https://github.githubassets.com/images/icons/emoji/unicode/1f48f.png?v8", - couplekiss_man_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2764-1f48b-1f468.png?v8", - couplekiss_man_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f48b-1f468.png?v8", - couplekiss_woman_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f48b-1f469.png?v8", - cow:"https://github.githubassets.com/images/icons/emoji/unicode/1f42e.png?v8", - cow2:"https://github.githubassets.com/images/icons/emoji/unicode/1f404.png?v8", - cowboy_hat_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f920.png?v8", - crab:"https://github.githubassets.com/images/icons/emoji/unicode/1f980.png?v8", - crayon:"https://github.githubassets.com/images/icons/emoji/unicode/1f58d.png?v8", - credit_card:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b3.png?v8", - crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f319.png?v8", - cricket:"https://github.githubassets.com/images/icons/emoji/unicode/1f997.png?v8", - cricket_game:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cf.png?v8", - croatia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f7.png?v8", - crocodile:"https://github.githubassets.com/images/icons/emoji/unicode/1f40a.png?v8", - croissant:"https://github.githubassets.com/images/icons/emoji/unicode/1f950.png?v8", - crossed_fingers:"https://github.githubassets.com/images/icons/emoji/unicode/1f91e.png?v8", - crossed_flags:"https://github.githubassets.com/images/icons/emoji/unicode/1f38c.png?v8", - crossed_swords:"https://github.githubassets.com/images/icons/emoji/unicode/2694.png?v8", - crown:"https://github.githubassets.com/images/icons/emoji/unicode/1f451.png?v8", - cry:"https://github.githubassets.com/images/icons/emoji/unicode/1f622.png?v8", - crying_cat_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f63f.png?v8", - crystal_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f52e.png?v8", - cuba:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fa.png?v8", - cucumber:"https://github.githubassets.com/images/icons/emoji/unicode/1f952.png?v8", - cup_with_straw:"https://github.githubassets.com/images/icons/emoji/unicode/1f964.png?v8", - cupcake:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c1.png?v8", - cupid:"https://github.githubassets.com/images/icons/emoji/unicode/1f498.png?v8", - curacao:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fc.png?v8", - curling_stone:"https://github.githubassets.com/images/icons/emoji/unicode/1f94c.png?v8", - curly_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b1.png?v8", - curly_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b1.png?v8", - curly_loop:"https://github.githubassets.com/images/icons/emoji/unicode/27b0.png?v8", - currency_exchange:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b1.png?v8", - curry:"https://github.githubassets.com/images/icons/emoji/unicode/1f35b.png?v8", - cursing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92c.png?v8", - custard:"https://github.githubassets.com/images/icons/emoji/unicode/1f36e.png?v8", - customs:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c3.png?v8", - cut_of_meat:"https://github.githubassets.com/images/icons/emoji/unicode/1f969.png?v8", - cyclone:"https://github.githubassets.com/images/icons/emoji/unicode/1f300.png?v8", - cyprus:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fe.png?v8", - czech_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ff.png?v8", - dagger:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e1.png?v8", - dancer:"https://github.githubassets.com/images/icons/emoji/unicode/1f483.png?v8", - dancers:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f.png?v8", - dancing_men:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f-2642.png?v8", - dancing_women:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f-2640.png?v8", - dango:"https://github.githubassets.com/images/icons/emoji/unicode/1f361.png?v8", - dark_sunglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f576.png?v8", - dart:"https://github.githubassets.com/images/icons/emoji/unicode/1f3af.png?v8", - dash:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a8.png?v8", - date:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c5.png?v8", - de:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ea.png?v8", - deaf_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf-2642.png?v8", - deaf_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf.png?v8", - deaf_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf-2640.png?v8", - deciduous_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f333.png?v8", - deer:"https://github.githubassets.com/images/icons/emoji/unicode/1f98c.png?v8", - denmark:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f0.png?v8", - department_store:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ec.png?v8", - derelict_house:"https://github.githubassets.com/images/icons/emoji/unicode/1f3da.png?v8", - desert:"https://github.githubassets.com/images/icons/emoji/unicode/1f3dc.png?v8", - desert_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f3dd.png?v8", - desktop_computer:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a5.png?v8", - detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575.png?v8", - diamond_shape_with_a_dot_inside:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a0.png?v8", - diamonds:"https://github.githubassets.com/images/icons/emoji/unicode/2666.png?v8", - diego_garcia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ec.png?v8", - disappointed:"https://github.githubassets.com/images/icons/emoji/unicode/1f61e.png?v8", - disappointed_relieved:"https://github.githubassets.com/images/icons/emoji/unicode/1f625.png?v8", - diving_mask:"https://github.githubassets.com/images/icons/emoji/unicode/1f93f.png?v8", - diya_lamp:"https://github.githubassets.com/images/icons/emoji/unicode/1fa94.png?v8", - dizzy:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ab.png?v8", - dizzy_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f635.png?v8", - djibouti:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ef.png?v8", - dna:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ec.png?v8", - do_not_litter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6af.png?v8", - dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f436.png?v8", - dog2:"https://github.githubassets.com/images/icons/emoji/unicode/1f415.png?v8", - dollar:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b5.png?v8", - dolls:"https://github.githubassets.com/images/icons/emoji/unicode/1f38e.png?v8", - dolphin:"https://github.githubassets.com/images/icons/emoji/unicode/1f42c.png?v8", - dominica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f2.png?v8", - dominican_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f4.png?v8", - door:"https://github.githubassets.com/images/icons/emoji/unicode/1f6aa.png?v8", - doughnut:"https://github.githubassets.com/images/icons/emoji/unicode/1f369.png?v8", - dove:"https://github.githubassets.com/images/icons/emoji/unicode/1f54a.png?v8", - dragon:"https://github.githubassets.com/images/icons/emoji/unicode/1f409.png?v8", - dragon_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f432.png?v8", - dress:"https://github.githubassets.com/images/icons/emoji/unicode/1f457.png?v8", - dromedary_camel:"https://github.githubassets.com/images/icons/emoji/unicode/1f42a.png?v8", - drooling_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f924.png?v8", - drop_of_blood:"https://github.githubassets.com/images/icons/emoji/unicode/1fa78.png?v8", - droplet:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a7.png?v8", - drum:"https://github.githubassets.com/images/icons/emoji/unicode/1f941.png?v8", - duck:"https://github.githubassets.com/images/icons/emoji/unicode/1f986.png?v8", - dumpling:"https://github.githubassets.com/images/icons/emoji/unicode/1f95f.png?v8", - dvd:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c0.png?v8", - "e-mail":"https://github.githubassets.com/images/icons/emoji/unicode/1f4e7.png?v8", - eagle:"https://github.githubassets.com/images/icons/emoji/unicode/1f985.png?v8", - ear:"https://github.githubassets.com/images/icons/emoji/unicode/1f442.png?v8", - ear_of_rice:"https://github.githubassets.com/images/icons/emoji/unicode/1f33e.png?v8", - ear_with_hearing_aid:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bb.png?v8", - earth_africa:"https://github.githubassets.com/images/icons/emoji/unicode/1f30d.png?v8", - earth_americas:"https://github.githubassets.com/images/icons/emoji/unicode/1f30e.png?v8", - earth_asia:"https://github.githubassets.com/images/icons/emoji/unicode/1f30f.png?v8", - ecuador:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1e8.png?v8", - egg:"https://github.githubassets.com/images/icons/emoji/unicode/1f95a.png?v8", - eggplant:"https://github.githubassets.com/images/icons/emoji/unicode/1f346.png?v8", - egypt:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ec.png?v8", - eight:"https://github.githubassets.com/images/icons/emoji/unicode/0038-20e3.png?v8", - eight_pointed_black_star:"https://github.githubassets.com/images/icons/emoji/unicode/2734.png?v8", - eight_spoked_asterisk:"https://github.githubassets.com/images/icons/emoji/unicode/2733.png?v8", - eject_button:"https://github.githubassets.com/images/icons/emoji/unicode/23cf.png?v8", - el_salvador:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fb.png?v8", - electric_plug:"https://github.githubassets.com/images/icons/emoji/unicode/1f50c.png?v8", - electron:"https://github.githubassets.com/images/icons/emoji/electron.png?v8", - elephant:"https://github.githubassets.com/images/icons/emoji/unicode/1f418.png?v8", - elf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd.png?v8", - elf_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd-2642.png?v8", - elf_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd-2640.png?v8", - email:"https://github.githubassets.com/images/icons/emoji/unicode/2709.png?v8", - end:"https://github.githubassets.com/images/icons/emoji/unicode/1f51a.png?v8", - england:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png?v8", - envelope:"https://github.githubassets.com/images/icons/emoji/unicode/2709.png?v8", - envelope_with_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e9.png?v8", - equatorial_guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f6.png?v8", - eritrea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f7.png?v8", - es:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f8.png?v8", - estonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ea.png?v8", - ethiopia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f9.png?v8", - eu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1fa.png?v8", - euro:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b6.png?v8", - european_castle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f0.png?v8", - european_post_office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e4.png?v8", - european_union:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1fa.png?v8", - evergreen_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f332.png?v8", - exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2757.png?v8", - exploding_head:"https://github.githubassets.com/images/icons/emoji/unicode/1f92f.png?v8", - expressionless:"https://github.githubassets.com/images/icons/emoji/unicode/1f611.png?v8", - eye:"https://github.githubassets.com/images/icons/emoji/unicode/1f441.png?v8", - eye_speech_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f441-1f5e8.png?v8", - eyeglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f453.png?v8", - eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f440.png?v8", - face_with_head_bandage:"https://github.githubassets.com/images/icons/emoji/unicode/1f915.png?v8", - face_with_thermometer:"https://github.githubassets.com/images/icons/emoji/unicode/1f912.png?v8", - facepalm:"https://github.githubassets.com/images/icons/emoji/unicode/1f926.png?v8", - facepunch:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8", - factory:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ed.png?v8", - factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3ed.png?v8", - fairy:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da.png?v8", - fairy_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da-2642.png?v8", - fairy_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da-2640.png?v8", - falafel:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c6.png?v8", - falkland_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f0.png?v8", - fallen_leaf:"https://github.githubassets.com/images/icons/emoji/unicode/1f342.png?v8", - family:"https://github.githubassets.com/images/icons/emoji/unicode/1f46a.png?v8", - family_man_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f466.png?v8", - family_man_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f466-1f466.png?v8", - family_man_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467.png?v8", - family_man_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467-1f466.png?v8", - family_man_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467-1f467.png?v8", - family_man_man_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f466.png?v8", - family_man_man_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f466-1f466.png?v8", - family_man_man_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467.png?v8", - family_man_man_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467-1f466.png?v8", - family_man_man_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467-1f467.png?v8", - family_man_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f466.png?v8", - family_man_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f466-1f466.png?v8", - family_man_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467.png?v8", - family_man_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467-1f466.png?v8", - family_man_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467-1f467.png?v8", - family_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f466.png?v8", - family_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f466-1f466.png?v8", - family_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467.png?v8", - family_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467-1f466.png?v8", - family_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467-1f467.png?v8", - family_woman_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f466.png?v8", - family_woman_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f466-1f466.png?v8", - family_woman_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467.png?v8", - family_woman_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467-1f466.png?v8", - family_woman_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467-1f467.png?v8", - farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f33e.png?v8", - faroe_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f4.png?v8", - fast_forward:"https://github.githubassets.com/images/icons/emoji/unicode/23e9.png?v8", - fax:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e0.png?v8", - fearful:"https://github.githubassets.com/images/icons/emoji/unicode/1f628.png?v8", - feelsgood:"https://github.githubassets.com/images/icons/emoji/feelsgood.png?v8", - feet:"https://github.githubassets.com/images/icons/emoji/unicode/1f43e.png?v8", - female_detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575-2640.png?v8", - female_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2640.png?v8", - ferris_wheel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a1.png?v8", - ferry:"https://github.githubassets.com/images/icons/emoji/unicode/26f4.png?v8", - field_hockey:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d1.png?v8", - fiji:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1ef.png?v8", - file_cabinet:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c4.png?v8", - file_folder:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c1.png?v8", - film_projector:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fd.png?v8", - film_strip:"https://github.githubassets.com/images/icons/emoji/unicode/1f39e.png?v8", - finland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1ee.png?v8", - finnadie:"https://github.githubassets.com/images/icons/emoji/finnadie.png?v8", - fire:"https://github.githubassets.com/images/icons/emoji/unicode/1f525.png?v8", - fire_engine:"https://github.githubassets.com/images/icons/emoji/unicode/1f692.png?v8", - fire_extinguisher:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ef.png?v8", - firecracker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e8.png?v8", - firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f692.png?v8", - fireworks:"https://github.githubassets.com/images/icons/emoji/unicode/1f386.png?v8", - first_quarter_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f313.png?v8", - first_quarter_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31b.png?v8", - fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f41f.png?v8", - fish_cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f365.png?v8", - fishing_pole_and_fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a3.png?v8", - fist:"https://github.githubassets.com/images/icons/emoji/unicode/270a.png?v8", - fist_left:"https://github.githubassets.com/images/icons/emoji/unicode/1f91b.png?v8", - fist_oncoming:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8", - fist_raised:"https://github.githubassets.com/images/icons/emoji/unicode/270a.png?v8", - fist_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f91c.png?v8", - five:"https://github.githubassets.com/images/icons/emoji/unicode/0035-20e3.png?v8", - flags:"https://github.githubassets.com/images/icons/emoji/unicode/1f38f.png?v8", - flamingo:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a9.png?v8", - flashlight:"https://github.githubassets.com/images/icons/emoji/unicode/1f526.png?v8", - flat_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f97f.png?v8", - fleur_de_lis:"https://github.githubassets.com/images/icons/emoji/unicode/269c.png?v8", - flight_arrival:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ec.png?v8", - flight_departure:"https://github.githubassets.com/images/icons/emoji/unicode/1f6eb.png?v8", - flipper:"https://github.githubassets.com/images/icons/emoji/unicode/1f42c.png?v8", - floppy_disk:"https://github.githubassets.com/images/icons/emoji/unicode/1f4be.png?v8", - flower_playing_cards:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b4.png?v8", - flushed:"https://github.githubassets.com/images/icons/emoji/unicode/1f633.png?v8", - flying_disc:"https://github.githubassets.com/images/icons/emoji/unicode/1f94f.png?v8", - flying_saucer:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f8.png?v8", - fog:"https://github.githubassets.com/images/icons/emoji/unicode/1f32b.png?v8", - foggy:"https://github.githubassets.com/images/icons/emoji/unicode/1f301.png?v8", - foot:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b6.png?v8", - football:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c8.png?v8", - footprints:"https://github.githubassets.com/images/icons/emoji/unicode/1f463.png?v8", - fork_and_knife:"https://github.githubassets.com/images/icons/emoji/unicode/1f374.png?v8", - fortune_cookie:"https://github.githubassets.com/images/icons/emoji/unicode/1f960.png?v8", - fountain:"https://github.githubassets.com/images/icons/emoji/unicode/26f2.png?v8", - fountain_pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f58b.png?v8", - four:"https://github.githubassets.com/images/icons/emoji/unicode/0034-20e3.png?v8", - four_leaf_clover:"https://github.githubassets.com/images/icons/emoji/unicode/1f340.png?v8", - fox_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f98a.png?v8", - fr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f7.png?v8", - framed_picture:"https://github.githubassets.com/images/icons/emoji/unicode/1f5bc.png?v8", - free:"https://github.githubassets.com/images/icons/emoji/unicode/1f193.png?v8", - french_guiana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1eb.png?v8", - french_polynesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1eb.png?v8", - french_southern_territories:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1eb.png?v8", - fried_egg:"https://github.githubassets.com/images/icons/emoji/unicode/1f373.png?v8", - fried_shrimp:"https://github.githubassets.com/images/icons/emoji/unicode/1f364.png?v8", - fries:"https://github.githubassets.com/images/icons/emoji/unicode/1f35f.png?v8", - frog:"https://github.githubassets.com/images/icons/emoji/unicode/1f438.png?v8", - frowning:"https://github.githubassets.com/images/icons/emoji/unicode/1f626.png?v8", - frowning_face:"https://github.githubassets.com/images/icons/emoji/unicode/2639.png?v8", - frowning_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d-2642.png?v8", - frowning_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d.png?v8", - frowning_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d-2640.png?v8", - fu:"https://github.githubassets.com/images/icons/emoji/unicode/1f595.png?v8", - fuelpump:"https://github.githubassets.com/images/icons/emoji/unicode/26fd.png?v8", - full_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f315.png?v8", - full_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31d.png?v8", - funeral_urn:"https://github.githubassets.com/images/icons/emoji/unicode/26b1.png?v8", - gabon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e6.png?v8", - gambia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f2.png?v8", - game_die:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b2.png?v8", - garlic:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c4.png?v8", - gb:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e7.png?v8", - gear:"https://github.githubassets.com/images/icons/emoji/unicode/2699.png?v8", - gem:"https://github.githubassets.com/images/icons/emoji/unicode/1f48e.png?v8", - gemini:"https://github.githubassets.com/images/icons/emoji/unicode/264a.png?v8", - genie:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de.png?v8", - genie_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de-2642.png?v8", - genie_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de-2640.png?v8", - georgia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ea.png?v8", - ghana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ed.png?v8", - ghost:"https://github.githubassets.com/images/icons/emoji/unicode/1f47b.png?v8", - gibraltar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ee.png?v8", - gift:"https://github.githubassets.com/images/icons/emoji/unicode/1f381.png?v8", - gift_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49d.png?v8", - giraffe:"https://github.githubassets.com/images/icons/emoji/unicode/1f992.png?v8", - girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f467.png?v8", - globe_with_meridians:"https://github.githubassets.com/images/icons/emoji/unicode/1f310.png?v8", - gloves:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e4.png?v8", - goal_net:"https://github.githubassets.com/images/icons/emoji/unicode/1f945.png?v8", - goat:"https://github.githubassets.com/images/icons/emoji/unicode/1f410.png?v8", - goberserk:"https://github.githubassets.com/images/icons/emoji/goberserk.png?v8", - godmode:"https://github.githubassets.com/images/icons/emoji/godmode.png?v8", - goggles:"https://github.githubassets.com/images/icons/emoji/unicode/1f97d.png?v8", - golf:"https://github.githubassets.com/images/icons/emoji/unicode/26f3.png?v8", - golfing:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc.png?v8", - golfing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc-2642.png?v8", - golfing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc-2640.png?v8", - gorilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f98d.png?v8", - grapes:"https://github.githubassets.com/images/icons/emoji/unicode/1f347.png?v8", - greece:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f7.png?v8", - green_apple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34f.png?v8", - green_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d7.png?v8", - green_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e2.png?v8", - green_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49a.png?v8", - green_salad:"https://github.githubassets.com/images/icons/emoji/unicode/1f957.png?v8", - green_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e9.png?v8", - greenland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f1.png?v8", - grenada:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e9.png?v8", - grey_exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2755.png?v8", - grey_question:"https://github.githubassets.com/images/icons/emoji/unicode/2754.png?v8", - grimacing:"https://github.githubassets.com/images/icons/emoji/unicode/1f62c.png?v8", - grin:"https://github.githubassets.com/images/icons/emoji/unicode/1f601.png?v8", - grinning:"https://github.githubassets.com/images/icons/emoji/unicode/1f600.png?v8", - guadeloupe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f5.png?v8", - guam:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fa.png?v8", - guard:"https://github.githubassets.com/images/icons/emoji/unicode/1f482.png?v8", - guardsman:"https://github.githubassets.com/images/icons/emoji/unicode/1f482-2642.png?v8", - guardswoman:"https://github.githubassets.com/images/icons/emoji/unicode/1f482-2640.png?v8", - guatemala:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f9.png?v8", - guernsey:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ec.png?v8", - guide_dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ae.png?v8", - guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f3.png?v8", - guinea_bissau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fc.png?v8", - guitar:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b8.png?v8", - gun:"https://github.githubassets.com/images/icons/emoji/unicode/1f52b.png?v8", - guyana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fe.png?v8", - haircut:"https://github.githubassets.com/images/icons/emoji/unicode/1f487.png?v8", - haircut_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f487-2642.png?v8", - haircut_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f487-2640.png?v8", - haiti:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f9.png?v8", - hamburger:"https://github.githubassets.com/images/icons/emoji/unicode/1f354.png?v8", - hammer:"https://github.githubassets.com/images/icons/emoji/unicode/1f528.png?v8", - hammer_and_pick:"https://github.githubassets.com/images/icons/emoji/unicode/2692.png?v8", - hammer_and_wrench:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e0.png?v8", - hamster:"https://github.githubassets.com/images/icons/emoji/unicode/1f439.png?v8", - hand:"https://github.githubassets.com/images/icons/emoji/unicode/270b.png?v8", - hand_over_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f92d.png?v8", - handbag:"https://github.githubassets.com/images/icons/emoji/unicode/1f45c.png?v8", - handball_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e.png?v8", - handshake:"https://github.githubassets.com/images/icons/emoji/unicode/1f91d.png?v8", - hankey:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8", - hash:"https://github.githubassets.com/images/icons/emoji/unicode/0023-20e3.png?v8", - hatched_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f425.png?v8", - hatching_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f423.png?v8", - headphones:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a7.png?v8", - health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2695.png?v8", - hear_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f649.png?v8", - heard_mcdonald_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f2.png?v8", - heart:"https://github.githubassets.com/images/icons/emoji/unicode/2764.png?v8", - heart_decoration:"https://github.githubassets.com/images/icons/emoji/unicode/1f49f.png?v8", - heart_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f60d.png?v8", - heart_eyes_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63b.png?v8", - heartbeat:"https://github.githubassets.com/images/icons/emoji/unicode/1f493.png?v8", - heartpulse:"https://github.githubassets.com/images/icons/emoji/unicode/1f497.png?v8", - hearts:"https://github.githubassets.com/images/icons/emoji/unicode/2665.png?v8", - heavy_check_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2714.png?v8", - heavy_division_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2797.png?v8", - heavy_dollar_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b2.png?v8", - heavy_exclamation_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2757.png?v8", - heavy_heart_exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2763.png?v8", - heavy_minus_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2796.png?v8", - heavy_multiplication_x:"https://github.githubassets.com/images/icons/emoji/unicode/2716.png?v8", - heavy_plus_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2795.png?v8", - hedgehog:"https://github.githubassets.com/images/icons/emoji/unicode/1f994.png?v8", - helicopter:"https://github.githubassets.com/images/icons/emoji/unicode/1f681.png?v8", - herb:"https://github.githubassets.com/images/icons/emoji/unicode/1f33f.png?v8", - hibiscus:"https://github.githubassets.com/images/icons/emoji/unicode/1f33a.png?v8", - high_brightness:"https://github.githubassets.com/images/icons/emoji/unicode/1f506.png?v8", - high_heel:"https://github.githubassets.com/images/icons/emoji/unicode/1f460.png?v8", - hiking_boot:"https://github.githubassets.com/images/icons/emoji/unicode/1f97e.png?v8", - hindu_temple:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d5.png?v8", - hippopotamus:"https://github.githubassets.com/images/icons/emoji/unicode/1f99b.png?v8", - hocho:"https://github.githubassets.com/images/icons/emoji/unicode/1f52a.png?v8", - hole:"https://github.githubassets.com/images/icons/emoji/unicode/1f573.png?v8", - honduras:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f3.png?v8", - honey_pot:"https://github.githubassets.com/images/icons/emoji/unicode/1f36f.png?v8", - honeybee:"https://github.githubassets.com/images/icons/emoji/unicode/1f41d.png?v8", - hong_kong:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f0.png?v8", - horse:"https://github.githubassets.com/images/icons/emoji/unicode/1f434.png?v8", - horse_racing:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c7.png?v8", - hospital:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e5.png?v8", - hot_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f975.png?v8", - hot_pepper:"https://github.githubassets.com/images/icons/emoji/unicode/1f336.png?v8", - hotdog:"https://github.githubassets.com/images/icons/emoji/unicode/1f32d.png?v8", - hotel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e8.png?v8", - hotsprings:"https://github.githubassets.com/images/icons/emoji/unicode/2668.png?v8", - hourglass:"https://github.githubassets.com/images/icons/emoji/unicode/231b.png?v8", - hourglass_flowing_sand:"https://github.githubassets.com/images/icons/emoji/unicode/23f3.png?v8", - house:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e0.png?v8", - house_with_garden:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e1.png?v8", - houses:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d8.png?v8", - hugs:"https://github.githubassets.com/images/icons/emoji/unicode/1f917.png?v8", - hungary:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1fa.png?v8", - hurtrealbad:"https://github.githubassets.com/images/icons/emoji/hurtrealbad.png?v8", - hushed:"https://github.githubassets.com/images/icons/emoji/unicode/1f62f.png?v8", - ice_cream:"https://github.githubassets.com/images/icons/emoji/unicode/1f368.png?v8", - ice_cube:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ca.png?v8", - ice_hockey:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d2.png?v8", - ice_skate:"https://github.githubassets.com/images/icons/emoji/unicode/26f8.png?v8", - icecream:"https://github.githubassets.com/images/icons/emoji/unicode/1f366.png?v8", - iceland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f8.png?v8", - id:"https://github.githubassets.com/images/icons/emoji/unicode/1f194.png?v8", - ideograph_advantage:"https://github.githubassets.com/images/icons/emoji/unicode/1f250.png?v8", - imp:"https://github.githubassets.com/images/icons/emoji/unicode/1f47f.png?v8", - inbox_tray:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e5.png?v8", - incoming_envelope:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e8.png?v8", - india:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f3.png?v8", - indonesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1e9.png?v8", - infinity:"https://github.githubassets.com/images/icons/emoji/unicode/267e.png?v8", - information_desk_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f481.png?v8", - information_source:"https://github.githubassets.com/images/icons/emoji/unicode/2139.png?v8", - innocent:"https://github.githubassets.com/images/icons/emoji/unicode/1f607.png?v8", - interrobang:"https://github.githubassets.com/images/icons/emoji/unicode/2049.png?v8", - iphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f1.png?v8", - iran:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f7.png?v8", - iraq:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f6.png?v8", - ireland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1ea.png?v8", - isle_of_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f2.png?v8", - israel:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f1.png?v8", - it:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f9.png?v8", - izakaya_lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ee.png?v8", - jack_o_lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f383.png?v8", - jamaica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f2.png?v8", - japan:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fe.png?v8", - japanese_castle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ef.png?v8", - japanese_goblin:"https://github.githubassets.com/images/icons/emoji/unicode/1f47a.png?v8", - japanese_ogre:"https://github.githubassets.com/images/icons/emoji/unicode/1f479.png?v8", - jeans:"https://github.githubassets.com/images/icons/emoji/unicode/1f456.png?v8", - jersey:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1ea.png?v8", - jigsaw:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e9.png?v8", - jordan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f4.png?v8", - joy:"https://github.githubassets.com/images/icons/emoji/unicode/1f602.png?v8", - joy_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f639.png?v8", - joystick:"https://github.githubassets.com/images/icons/emoji/unicode/1f579.png?v8", - jp:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f5.png?v8", - judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2696.png?v8", - juggling_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f939.png?v8", - kaaba:"https://github.githubassets.com/images/icons/emoji/unicode/1f54b.png?v8", - kangaroo:"https://github.githubassets.com/images/icons/emoji/unicode/1f998.png?v8", - kazakhstan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ff.png?v8", - kenya:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ea.png?v8", - key:"https://github.githubassets.com/images/icons/emoji/unicode/1f511.png?v8", - keyboard:"https://github.githubassets.com/images/icons/emoji/unicode/2328.png?v8", - keycap_ten:"https://github.githubassets.com/images/icons/emoji/unicode/1f51f.png?v8", - kick_scooter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f4.png?v8", - kimono:"https://github.githubassets.com/images/icons/emoji/unicode/1f458.png?v8", - kiribati:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ee.png?v8", - kiss:"https://github.githubassets.com/images/icons/emoji/unicode/1f48b.png?v8", - kissing:"https://github.githubassets.com/images/icons/emoji/unicode/1f617.png?v8", - kissing_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63d.png?v8", - kissing_closed_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f61a.png?v8", - kissing_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f618.png?v8", - kissing_smiling_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f619.png?v8", - kite:"https://github.githubassets.com/images/icons/emoji/unicode/1fa81.png?v8", - kiwi_fruit:"https://github.githubassets.com/images/icons/emoji/unicode/1f95d.png?v8", - kneeling_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce-2642.png?v8", - kneeling_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce.png?v8", - kneeling_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce-2640.png?v8", - knife:"https://github.githubassets.com/images/icons/emoji/unicode/1f52a.png?v8", - koala:"https://github.githubassets.com/images/icons/emoji/unicode/1f428.png?v8", - koko:"https://github.githubassets.com/images/icons/emoji/unicode/1f201.png?v8", - kosovo:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fd-1f1f0.png?v8", - kr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f7.png?v8", - kuwait:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1fc.png?v8", - kyrgyzstan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ec.png?v8", - lab_coat:"https://github.githubassets.com/images/icons/emoji/unicode/1f97c.png?v8", - label:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f7.png?v8", - lacrosse:"https://github.githubassets.com/images/icons/emoji/unicode/1f94d.png?v8", - lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ee.png?v8", - laos:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e6.png?v8", - large_blue_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f535.png?v8", - large_blue_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f537.png?v8", - large_orange_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f536.png?v8", - last_quarter_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f317.png?v8", - last_quarter_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31c.png?v8", - latin_cross:"https://github.githubassets.com/images/icons/emoji/unicode/271d.png?v8", - latvia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fb.png?v8", - laughing:"https://github.githubassets.com/images/icons/emoji/unicode/1f606.png?v8", - leafy_green:"https://github.githubassets.com/images/icons/emoji/unicode/1f96c.png?v8", - leaves:"https://github.githubassets.com/images/icons/emoji/unicode/1f343.png?v8", - lebanon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e7.png?v8", - ledger:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d2.png?v8", - left_luggage:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c5.png?v8", - left_right_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/2194.png?v8", - left_speech_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e8.png?v8", - leftwards_arrow_with_hook:"https://github.githubassets.com/images/icons/emoji/unicode/21a9.png?v8", - leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b5.png?v8", - lemon:"https://github.githubassets.com/images/icons/emoji/unicode/1f34b.png?v8", - leo:"https://github.githubassets.com/images/icons/emoji/unicode/264c.png?v8", - leopard:"https://github.githubassets.com/images/icons/emoji/unicode/1f406.png?v8", - lesotho:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f8.png?v8", - level_slider:"https://github.githubassets.com/images/icons/emoji/unicode/1f39a.png?v8", - liberia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f7.png?v8", - libra:"https://github.githubassets.com/images/icons/emoji/unicode/264e.png?v8", - libya:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fe.png?v8", - liechtenstein:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1ee.png?v8", - light_rail:"https://github.githubassets.com/images/icons/emoji/unicode/1f688.png?v8", - link:"https://github.githubassets.com/images/icons/emoji/unicode/1f517.png?v8", - lion:"https://github.githubassets.com/images/icons/emoji/unicode/1f981.png?v8", - lips:"https://github.githubassets.com/images/icons/emoji/unicode/1f444.png?v8", - lipstick:"https://github.githubassets.com/images/icons/emoji/unicode/1f484.png?v8", - lithuania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f9.png?v8", - lizard:"https://github.githubassets.com/images/icons/emoji/unicode/1f98e.png?v8", - llama:"https://github.githubassets.com/images/icons/emoji/unicode/1f999.png?v8", - lobster:"https://github.githubassets.com/images/icons/emoji/unicode/1f99e.png?v8", - lock:"https://github.githubassets.com/images/icons/emoji/unicode/1f512.png?v8", - lock_with_ink_pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f50f.png?v8", - lollipop:"https://github.githubassets.com/images/icons/emoji/unicode/1f36d.png?v8", - loop:"https://github.githubassets.com/images/icons/emoji/unicode/27bf.png?v8", - lotion_bottle:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f4.png?v8", - lotus_position:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8.png?v8", - lotus_position_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8-2642.png?v8", - lotus_position_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8-2640.png?v8", - loud_sound:"https://github.githubassets.com/images/icons/emoji/unicode/1f50a.png?v8", - loudspeaker:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e2.png?v8", - love_hotel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e9.png?v8", - love_letter:"https://github.githubassets.com/images/icons/emoji/unicode/1f48c.png?v8", - love_you_gesture:"https://github.githubassets.com/images/icons/emoji/unicode/1f91f.png?v8", - low_brightness:"https://github.githubassets.com/images/icons/emoji/unicode/1f505.png?v8", - luggage:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f3.png?v8", - luxembourg:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fa.png?v8", - lying_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f925.png?v8", - m:"https://github.githubassets.com/images/icons/emoji/unicode/24c2.png?v8", - macau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f4.png?v8", - macedonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f0.png?v8", - madagascar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ec.png?v8", - mag:"https://github.githubassets.com/images/icons/emoji/unicode/1f50d.png?v8", - mag_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f50e.png?v8", - mage:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9.png?v8", - mage_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9-2642.png?v8", - mage_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9-2640.png?v8", - magnet:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f2.png?v8", - mahjong:"https://github.githubassets.com/images/icons/emoji/unicode/1f004.png?v8", - mailbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f4eb.png?v8", - mailbox_closed:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ea.png?v8", - mailbox_with_mail:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ec.png?v8", - mailbox_with_no_mail:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ed.png?v8", - malawi:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fc.png?v8", - malaysia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fe.png?v8", - maldives:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fb.png?v8", - male_detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575-2642.png?v8", - male_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2642.png?v8", - mali:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f1.png?v8", - malta:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f9.png?v8", - man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468.png?v8", - man_artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3a8.png?v8", - man_astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f680.png?v8", - man_cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938-2642.png?v8", - man_cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f373.png?v8", - man_dancing:"https://github.githubassets.com/images/icons/emoji/unicode/1f57a.png?v8", - man_facepalming:"https://github.githubassets.com/images/icons/emoji/unicode/1f926-2642.png?v8", - man_factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3ed.png?v8", - man_farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f33e.png?v8", - man_firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f692.png?v8", - man_health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2695.png?v8", - man_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9bd.png?v8", - man_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9bc.png?v8", - man_in_tuxedo:"https://github.githubassets.com/images/icons/emoji/unicode/1f935.png?v8", - man_judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2696.png?v8", - man_juggling:"https://github.githubassets.com/images/icons/emoji/unicode/1f939-2642.png?v8", - man_mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f527.png?v8", - man_office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f4bc.png?v8", - man_pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2708.png?v8", - man_playing_handball:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e-2642.png?v8", - man_playing_water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d-2642.png?v8", - man_scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f52c.png?v8", - man_shrugging:"https://github.githubassets.com/images/icons/emoji/unicode/1f937-2642.png?v8", - man_singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3a4.png?v8", - man_student:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f393.png?v8", - man_teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3eb.png?v8", - man_technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f4bb.png?v8", - man_with_gua_pi_mao:"https://github.githubassets.com/images/icons/emoji/unicode/1f472.png?v8", - man_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9af.png?v8", - man_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473-2642.png?v8", - mandarin:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8", - mango:"https://github.githubassets.com/images/icons/emoji/unicode/1f96d.png?v8", - mans_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45e.png?v8", - mantelpiece_clock:"https://github.githubassets.com/images/icons/emoji/unicode/1f570.png?v8", - manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bd.png?v8", - maple_leaf:"https://github.githubassets.com/images/icons/emoji/unicode/1f341.png?v8", - marshall_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ed.png?v8", - martial_arts_uniform:"https://github.githubassets.com/images/icons/emoji/unicode/1f94b.png?v8", - martinique:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f6.png?v8", - mask:"https://github.githubassets.com/images/icons/emoji/unicode/1f637.png?v8", - massage:"https://github.githubassets.com/images/icons/emoji/unicode/1f486.png?v8", - massage_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f486-2642.png?v8", - massage_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f486-2640.png?v8", - mate:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c9.png?v8", - mauritania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f7.png?v8", - mauritius:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fa.png?v8", - mayotte:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fe-1f1f9.png?v8", - meat_on_bone:"https://github.githubassets.com/images/icons/emoji/unicode/1f356.png?v8", - mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f527.png?v8", - mechanical_arm:"https://github.githubassets.com/images/icons/emoji/unicode/1f9be.png?v8", - mechanical_leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bf.png?v8", - medal_military:"https://github.githubassets.com/images/icons/emoji/unicode/1f396.png?v8", - medal_sports:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c5.png?v8", - medical_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/2695.png?v8", - mega:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e3.png?v8", - melon:"https://github.githubassets.com/images/icons/emoji/unicode/1f348.png?v8", - memo:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dd.png?v8", - men_wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c-2642.png?v8", - menorah:"https://github.githubassets.com/images/icons/emoji/unicode/1f54e.png?v8", - mens:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b9.png?v8", - mermaid:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc-2640.png?v8", - merman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc-2642.png?v8", - merperson:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc.png?v8", - metal:"https://github.githubassets.com/images/icons/emoji/unicode/1f918.png?v8", - metro:"https://github.githubassets.com/images/icons/emoji/unicode/1f687.png?v8", - mexico:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fd.png?v8", - microbe:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a0.png?v8", - micronesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f2.png?v8", - microphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a4.png?v8", - microscope:"https://github.githubassets.com/images/icons/emoji/unicode/1f52c.png?v8", - middle_finger:"https://github.githubassets.com/images/icons/emoji/unicode/1f595.png?v8", - milk_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f95b.png?v8", - milky_way:"https://github.githubassets.com/images/icons/emoji/unicode/1f30c.png?v8", - minibus:"https://github.githubassets.com/images/icons/emoji/unicode/1f690.png?v8", - minidisc:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bd.png?v8", - mobile_phone_off:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f4.png?v8", - moldova:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e9.png?v8", - monaco:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e8.png?v8", - money_mouth_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f911.png?v8", - money_with_wings:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b8.png?v8", - moneybag:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b0.png?v8", - mongolia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f3.png?v8", - monkey:"https://github.githubassets.com/images/icons/emoji/unicode/1f412.png?v8", - monkey_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f435.png?v8", - monocle_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d0.png?v8", - monorail:"https://github.githubassets.com/images/icons/emoji/unicode/1f69d.png?v8", - montenegro:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ea.png?v8", - montserrat:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f8.png?v8", - moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f314.png?v8", - moon_cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f96e.png?v8", - morocco:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e6.png?v8", - mortar_board:"https://github.githubassets.com/images/icons/emoji/unicode/1f393.png?v8", - mosque:"https://github.githubassets.com/images/icons/emoji/unicode/1f54c.png?v8", - mosquito:"https://github.githubassets.com/images/icons/emoji/unicode/1f99f.png?v8", - motor_boat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e5.png?v8", - motor_scooter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f5.png?v8", - motorcycle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cd.png?v8", - motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bc.png?v8", - motorway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e3.png?v8", - mount_fuji:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fb.png?v8", - mountain:"https://github.githubassets.com/images/icons/emoji/unicode/26f0.png?v8", - mountain_bicyclist:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5.png?v8", - mountain_biking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5-2642.png?v8", - mountain_biking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5-2640.png?v8", - mountain_cableway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a0.png?v8", - mountain_railway:"https://github.githubassets.com/images/icons/emoji/unicode/1f69e.png?v8", - mountain_snow:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d4.png?v8", - mouse:"https://github.githubassets.com/images/icons/emoji/unicode/1f42d.png?v8", - mouse2:"https://github.githubassets.com/images/icons/emoji/unicode/1f401.png?v8", - movie_camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a5.png?v8", - moyai:"https://github.githubassets.com/images/icons/emoji/unicode/1f5ff.png?v8", - mozambique:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ff.png?v8", - mrs_claus:"https://github.githubassets.com/images/icons/emoji/unicode/1f936.png?v8", - muscle:"https://github.githubassets.com/images/icons/emoji/unicode/1f4aa.png?v8", - mushroom:"https://github.githubassets.com/images/icons/emoji/unicode/1f344.png?v8", - musical_keyboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b9.png?v8", - musical_note:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b5.png?v8", - musical_score:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bc.png?v8", - mute:"https://github.githubassets.com/images/icons/emoji/unicode/1f507.png?v8", - myanmar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f2.png?v8", - nail_care:"https://github.githubassets.com/images/icons/emoji/unicode/1f485.png?v8", - name_badge:"https://github.githubassets.com/images/icons/emoji/unicode/1f4db.png?v8", - namibia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1e6.png?v8", - national_park:"https://github.githubassets.com/images/icons/emoji/unicode/1f3de.png?v8", - nauru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f7.png?v8", - nauseated_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f922.png?v8", - nazar_amulet:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ff.png?v8", - neckbeard:"https://github.githubassets.com/images/icons/emoji/neckbeard.png?v8", - necktie:"https://github.githubassets.com/images/icons/emoji/unicode/1f454.png?v8", - negative_squared_cross_mark:"https://github.githubassets.com/images/icons/emoji/unicode/274e.png?v8", - nepal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f5.png?v8", - nerd_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f913.png?v8", - netherlands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f1.png?v8", - neutral_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f610.png?v8", - new:"https://github.githubassets.com/images/icons/emoji/unicode/1f195.png?v8", - new_caledonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1e8.png?v8", - new_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f311.png?v8", - new_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31a.png?v8", - new_zealand:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ff.png?v8", - newspaper:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f0.png?v8", - newspaper_roll:"https://github.githubassets.com/images/icons/emoji/unicode/1f5de.png?v8", - next_track_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ed.png?v8", - ng:"https://github.githubassets.com/images/icons/emoji/unicode/1f196.png?v8", - ng_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2642.png?v8", - ng_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2640.png?v8", - nicaragua:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ee.png?v8", - niger:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ea.png?v8", - nigeria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ec.png?v8", - night_with_stars:"https://github.githubassets.com/images/icons/emoji/unicode/1f303.png?v8", - nine:"https://github.githubassets.com/images/icons/emoji/unicode/0039-20e3.png?v8", - niue:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1fa.png?v8", - no_bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f515.png?v8", - no_bicycles:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b3.png?v8", - no_entry:"https://github.githubassets.com/images/icons/emoji/unicode/26d4.png?v8", - no_entry_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ab.png?v8", - no_good:"https://github.githubassets.com/images/icons/emoji/unicode/1f645.png?v8", - no_good_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2642.png?v8", - no_good_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2640.png?v8", - no_mobile_phones:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f5.png?v8", - no_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f636.png?v8", - no_pedestrians:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b7.png?v8", - no_smoking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ad.png?v8", - "non-potable_water":"https://github.githubassets.com/images/icons/emoji/unicode/1f6b1.png?v8", - norfolk_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1eb.png?v8", - north_korea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f5.png?v8", - northern_mariana_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f5.png?v8", - norway:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f4.png?v8", - nose:"https://github.githubassets.com/images/icons/emoji/unicode/1f443.png?v8", - notebook:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d3.png?v8", - notebook_with_decorative_cover:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d4.png?v8", - notes:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b6.png?v8", - nut_and_bolt:"https://github.githubassets.com/images/icons/emoji/unicode/1f529.png?v8", - o:"https://github.githubassets.com/images/icons/emoji/unicode/2b55.png?v8", - o2:"https://github.githubassets.com/images/icons/emoji/unicode/1f17e.png?v8", - ocean:"https://github.githubassets.com/images/icons/emoji/unicode/1f30a.png?v8", - octocat:"https://github.githubassets.com/images/icons/emoji/octocat.png?v8", - octopus:"https://github.githubassets.com/images/icons/emoji/unicode/1f419.png?v8", - oden:"https://github.githubassets.com/images/icons/emoji/unicode/1f362.png?v8", - office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e2.png?v8", - office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f4bc.png?v8", - oil_drum:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e2.png?v8", - ok:"https://github.githubassets.com/images/icons/emoji/unicode/1f197.png?v8", - ok_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f44c.png?v8", - ok_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f646-2642.png?v8", - ok_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f646.png?v8", - ok_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f646-2640.png?v8", - old_key:"https://github.githubassets.com/images/icons/emoji/unicode/1f5dd.png?v8", - older_adult:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d3.png?v8", - older_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f474.png?v8", - older_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f475.png?v8", - om:"https://github.githubassets.com/images/icons/emoji/unicode/1f549.png?v8", - oman:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f4-1f1f2.png?v8", - on:"https://github.githubassets.com/images/icons/emoji/unicode/1f51b.png?v8", - oncoming_automobile:"https://github.githubassets.com/images/icons/emoji/unicode/1f698.png?v8", - oncoming_bus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68d.png?v8", - oncoming_police_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f694.png?v8", - oncoming_taxi:"https://github.githubassets.com/images/icons/emoji/unicode/1f696.png?v8", - one:"https://github.githubassets.com/images/icons/emoji/unicode/0031-20e3.png?v8", - one_piece_swimsuit:"https://github.githubassets.com/images/icons/emoji/unicode/1fa71.png?v8", - onion:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c5.png?v8", - open_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d6.png?v8", - open_file_folder:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c2.png?v8", - open_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f450.png?v8", - open_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f62e.png?v8", - open_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/2602.png?v8", - ophiuchus:"https://github.githubassets.com/images/icons/emoji/unicode/26ce.png?v8", - orange:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8", - orange_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d9.png?v8", - orange_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e0.png?v8", - orange_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e1.png?v8", - orange_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e7.png?v8", - orangutan:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a7.png?v8", - orthodox_cross:"https://github.githubassets.com/images/icons/emoji/unicode/2626.png?v8", - otter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a6.png?v8", - outbox_tray:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e4.png?v8", - owl:"https://github.githubassets.com/images/icons/emoji/unicode/1f989.png?v8", - ox:"https://github.githubassets.com/images/icons/emoji/unicode/1f402.png?v8", - oyster:"https://github.githubassets.com/images/icons/emoji/unicode/1f9aa.png?v8", - package:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e6.png?v8", - page_facing_up:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c4.png?v8", - page_with_curl:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c3.png?v8", - pager:"https://github.githubassets.com/images/icons/emoji/unicode/1f4df.png?v8", - paintbrush:"https://github.githubassets.com/images/icons/emoji/unicode/1f58c.png?v8", - pakistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f0.png?v8", - palau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1fc.png?v8", - palestinian_territories:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f8.png?v8", - palm_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f334.png?v8", - palms_up_together:"https://github.githubassets.com/images/icons/emoji/unicode/1f932.png?v8", - panama:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1e6.png?v8", - pancakes:"https://github.githubassets.com/images/icons/emoji/unicode/1f95e.png?v8", - panda_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f43c.png?v8", - paperclip:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ce.png?v8", - paperclips:"https://github.githubassets.com/images/icons/emoji/unicode/1f587.png?v8", - papua_new_guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ec.png?v8", - parachute:"https://github.githubassets.com/images/icons/emoji/unicode/1fa82.png?v8", - paraguay:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1fe.png?v8", - parasol_on_ground:"https://github.githubassets.com/images/icons/emoji/unicode/26f1.png?v8", - parking:"https://github.githubassets.com/images/icons/emoji/unicode/1f17f.png?v8", - parrot:"https://github.githubassets.com/images/icons/emoji/unicode/1f99c.png?v8", - part_alternation_mark:"https://github.githubassets.com/images/icons/emoji/unicode/303d.png?v8", - partly_sunny:"https://github.githubassets.com/images/icons/emoji/unicode/26c5.png?v8", - partying_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f973.png?v8", - passenger_ship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f3.png?v8", - passport_control:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c2.png?v8", - pause_button:"https://github.githubassets.com/images/icons/emoji/unicode/23f8.png?v8", - paw_prints:"https://github.githubassets.com/images/icons/emoji/unicode/1f43e.png?v8", - peace_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/262e.png?v8", - peach:"https://github.githubassets.com/images/icons/emoji/unicode/1f351.png?v8", - peacock:"https://github.githubassets.com/images/icons/emoji/unicode/1f99a.png?v8", - peanuts:"https://github.githubassets.com/images/icons/emoji/unicode/1f95c.png?v8", - pear:"https://github.githubassets.com/images/icons/emoji/unicode/1f350.png?v8", - pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f58a.png?v8", - pencil:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dd.png?v8", - pencil2:"https://github.githubassets.com/images/icons/emoji/unicode/270f.png?v8", - penguin:"https://github.githubassets.com/images/icons/emoji/unicode/1f427.png?v8", - pensive:"https://github.githubassets.com/images/icons/emoji/unicode/1f614.png?v8", - people_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f91d-1f9d1.png?v8", - performing_arts:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ad.png?v8", - persevere:"https://github.githubassets.com/images/icons/emoji/unicode/1f623.png?v8", - person_bald:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b2.png?v8", - person_curly_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b1.png?v8", - person_fencing:"https://github.githubassets.com/images/icons/emoji/unicode/1f93a.png?v8", - person_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9bd.png?v8", - person_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9bc.png?v8", - person_red_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b0.png?v8", - person_white_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b3.png?v8", - person_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9af.png?v8", - person_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473.png?v8", - peru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ea.png?v8", - petri_dish:"https://github.githubassets.com/images/icons/emoji/unicode/1f9eb.png?v8", - philippines:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ed.png?v8", - phone:"https://github.githubassets.com/images/icons/emoji/unicode/260e.png?v8", - pick:"https://github.githubassets.com/images/icons/emoji/unicode/26cf.png?v8", - pie:"https://github.githubassets.com/images/icons/emoji/unicode/1f967.png?v8", - pig:"https://github.githubassets.com/images/icons/emoji/unicode/1f437.png?v8", - pig2:"https://github.githubassets.com/images/icons/emoji/unicode/1f416.png?v8", - pig_nose:"https://github.githubassets.com/images/icons/emoji/unicode/1f43d.png?v8", - pill:"https://github.githubassets.com/images/icons/emoji/unicode/1f48a.png?v8", - pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2708.png?v8", - pinching_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f90f.png?v8", - pineapple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34d.png?v8", - ping_pong:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d3.png?v8", - pirate_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-2620.png?v8", - pisces:"https://github.githubassets.com/images/icons/emoji/unicode/2653.png?v8", - pitcairn_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f3.png?v8", - pizza:"https://github.githubassets.com/images/icons/emoji/unicode/1f355.png?v8", - place_of_worship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d0.png?v8", - plate_with_cutlery:"https://github.githubassets.com/images/icons/emoji/unicode/1f37d.png?v8", - play_or_pause_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ef.png?v8", - pleading_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f97a.png?v8", - point_down:"https://github.githubassets.com/images/icons/emoji/unicode/1f447.png?v8", - point_left:"https://github.githubassets.com/images/icons/emoji/unicode/1f448.png?v8", - point_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f449.png?v8", - point_up:"https://github.githubassets.com/images/icons/emoji/unicode/261d.png?v8", - point_up_2:"https://github.githubassets.com/images/icons/emoji/unicode/1f446.png?v8", - poland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f1.png?v8", - police_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f693.png?v8", - police_officer:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e.png?v8", - policeman:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e-2642.png?v8", - policewoman:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e-2640.png?v8", - poodle:"https://github.githubassets.com/images/icons/emoji/unicode/1f429.png?v8", - poop:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8", - popcorn:"https://github.githubassets.com/images/icons/emoji/unicode/1f37f.png?v8", - portugal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f9.png?v8", - post_office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e3.png?v8", - postal_horn:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ef.png?v8", - postbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ee.png?v8", - potable_water:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b0.png?v8", - potato:"https://github.githubassets.com/images/icons/emoji/unicode/1f954.png?v8", - pouch:"https://github.githubassets.com/images/icons/emoji/unicode/1f45d.png?v8", - poultry_leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f357.png?v8", - pound:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b7.png?v8", - pout:"https://github.githubassets.com/images/icons/emoji/unicode/1f621.png?v8", - pouting_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63e.png?v8", - pouting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e.png?v8", - pouting_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e-2642.png?v8", - pouting_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e-2640.png?v8", - pray:"https://github.githubassets.com/images/icons/emoji/unicode/1f64f.png?v8", - prayer_beads:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ff.png?v8", - pregnant_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f930.png?v8", - pretzel:"https://github.githubassets.com/images/icons/emoji/unicode/1f968.png?v8", - previous_track_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ee.png?v8", - prince:"https://github.githubassets.com/images/icons/emoji/unicode/1f934.png?v8", - princess:"https://github.githubassets.com/images/icons/emoji/unicode/1f478.png?v8", - printer:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a8.png?v8", - probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f9af.png?v8", - puerto_rico:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f7.png?v8", - punch:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8", - purple_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e3.png?v8", - purple_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49c.png?v8", - purple_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7ea.png?v8", - purse:"https://github.githubassets.com/images/icons/emoji/unicode/1f45b.png?v8", - pushpin:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cc.png?v8", - put_litter_in_its_place:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ae.png?v8", - qatar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f6-1f1e6.png?v8", - question:"https://github.githubassets.com/images/icons/emoji/unicode/2753.png?v8", - rabbit:"https://github.githubassets.com/images/icons/emoji/unicode/1f430.png?v8", - rabbit2:"https://github.githubassets.com/images/icons/emoji/unicode/1f407.png?v8", - raccoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f99d.png?v8", - racehorse:"https://github.githubassets.com/images/icons/emoji/unicode/1f40e.png?v8", - racing_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ce.png?v8", - radio:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fb.png?v8", - radio_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f518.png?v8", - radioactive:"https://github.githubassets.com/images/icons/emoji/unicode/2622.png?v8", - rage:"https://github.githubassets.com/images/icons/emoji/unicode/1f621.png?v8", - rage1:"https://github.githubassets.com/images/icons/emoji/rage1.png?v8", - rage2:"https://github.githubassets.com/images/icons/emoji/rage2.png?v8", - rage3:"https://github.githubassets.com/images/icons/emoji/rage3.png?v8", - rage4:"https://github.githubassets.com/images/icons/emoji/rage4.png?v8", - railway_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f683.png?v8", - railway_track:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e4.png?v8", - rainbow:"https://github.githubassets.com/images/icons/emoji/unicode/1f308.png?v8", - rainbow_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f3-1f308.png?v8", - raised_back_of_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f91a.png?v8", - raised_eyebrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f928.png?v8", - raised_hand:"https://github.githubassets.com/images/icons/emoji/unicode/270b.png?v8", - raised_hand_with_fingers_splayed:"https://github.githubassets.com/images/icons/emoji/unicode/1f590.png?v8", - raised_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f64c.png?v8", - raising_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b.png?v8", - raising_hand_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b-2642.png?v8", - raising_hand_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b-2640.png?v8", - ram:"https://github.githubassets.com/images/icons/emoji/unicode/1f40f.png?v8", - ramen:"https://github.githubassets.com/images/icons/emoji/unicode/1f35c.png?v8", - rat:"https://github.githubassets.com/images/icons/emoji/unicode/1f400.png?v8", - razor:"https://github.githubassets.com/images/icons/emoji/unicode/1fa92.png?v8", - receipt:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fe.png?v8", - record_button:"https://github.githubassets.com/images/icons/emoji/unicode/23fa.png?v8", - recycle:"https://github.githubassets.com/images/icons/emoji/unicode/267b.png?v8", - red_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f697.png?v8", - red_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f534.png?v8", - red_envelope:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e7.png?v8", - red_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b0.png?v8", - red_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b0.png?v8", - red_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e5.png?v8", - registered:"https://github.githubassets.com/images/icons/emoji/unicode/00ae.png?v8", - relaxed:"https://github.githubassets.com/images/icons/emoji/unicode/263a.png?v8", - relieved:"https://github.githubassets.com/images/icons/emoji/unicode/1f60c.png?v8", - reminder_ribbon:"https://github.githubassets.com/images/icons/emoji/unicode/1f397.png?v8", - repeat:"https://github.githubassets.com/images/icons/emoji/unicode/1f501.png?v8", - repeat_one:"https://github.githubassets.com/images/icons/emoji/unicode/1f502.png?v8", - rescue_worker_helmet:"https://github.githubassets.com/images/icons/emoji/unicode/26d1.png?v8", - restroom:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bb.png?v8", - reunion:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1ea.png?v8", - revolving_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f49e.png?v8", - rewind:"https://github.githubassets.com/images/icons/emoji/unicode/23ea.png?v8", - rhinoceros:"https://github.githubassets.com/images/icons/emoji/unicode/1f98f.png?v8", - ribbon:"https://github.githubassets.com/images/icons/emoji/unicode/1f380.png?v8", - rice:"https://github.githubassets.com/images/icons/emoji/unicode/1f35a.png?v8", - rice_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f359.png?v8", - rice_cracker:"https://github.githubassets.com/images/icons/emoji/unicode/1f358.png?v8", - rice_scene:"https://github.githubassets.com/images/icons/emoji/unicode/1f391.png?v8", - right_anger_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f5ef.png?v8", - ring:"https://github.githubassets.com/images/icons/emoji/unicode/1f48d.png?v8", - ringed_planet:"https://github.githubassets.com/images/icons/emoji/unicode/1fa90.png?v8", - robot:"https://github.githubassets.com/images/icons/emoji/unicode/1f916.png?v8", - rocket:"https://github.githubassets.com/images/icons/emoji/unicode/1f680.png?v8", - rofl:"https://github.githubassets.com/images/icons/emoji/unicode/1f923.png?v8", - roll_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f644.png?v8", - roll_of_paper:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fb.png?v8", - roller_coaster:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a2.png?v8", - romania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1f4.png?v8", - rooster:"https://github.githubassets.com/images/icons/emoji/unicode/1f413.png?v8", - rose:"https://github.githubassets.com/images/icons/emoji/unicode/1f339.png?v8", - rosette:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f5.png?v8", - rotating_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a8.png?v8", - round_pushpin:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cd.png?v8", - rowboat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3.png?v8", - rowing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3-2642.png?v8", - rowing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3-2640.png?v8", - ru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1fa.png?v8", - rugby_football:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c9.png?v8", - runner:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3.png?v8", - running:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3.png?v8", - running_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3-2642.png?v8", - running_shirt_with_sash:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bd.png?v8", - running_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3-2640.png?v8", - rwanda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1fc.png?v8", - sa:"https://github.githubassets.com/images/icons/emoji/unicode/1f202.png?v8", - safety_pin:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f7.png?v8", - safety_vest:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ba.png?v8", - sagittarius:"https://github.githubassets.com/images/icons/emoji/unicode/2650.png?v8", - sailboat:"https://github.githubassets.com/images/icons/emoji/unicode/26f5.png?v8", - sake:"https://github.githubassets.com/images/icons/emoji/unicode/1f376.png?v8", - salt:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c2.png?v8", - samoa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fc-1f1f8.png?v8", - san_marino:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f2.png?v8", - sandal:"https://github.githubassets.com/images/icons/emoji/unicode/1f461.png?v8", - sandwich:"https://github.githubassets.com/images/icons/emoji/unicode/1f96a.png?v8", - santa:"https://github.githubassets.com/images/icons/emoji/unicode/1f385.png?v8", - sao_tome_principe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f9.png?v8", - sari:"https://github.githubassets.com/images/icons/emoji/unicode/1f97b.png?v8", - sassy_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2642.png?v8", - sassy_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2640.png?v8", - satellite:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e1.png?v8", - satisfied:"https://github.githubassets.com/images/icons/emoji/unicode/1f606.png?v8", - saudi_arabia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e6.png?v8", - sauna_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6-2642.png?v8", - sauna_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6.png?v8", - sauna_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6-2640.png?v8", - sauropod:"https://github.githubassets.com/images/icons/emoji/unicode/1f995.png?v8", - saxophone:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b7.png?v8", - scarf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e3.png?v8", - school:"https://github.githubassets.com/images/icons/emoji/unicode/1f3eb.png?v8", - school_satchel:"https://github.githubassets.com/images/icons/emoji/unicode/1f392.png?v8", - scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f52c.png?v8", - scissors:"https://github.githubassets.com/images/icons/emoji/unicode/2702.png?v8", - scorpion:"https://github.githubassets.com/images/icons/emoji/unicode/1f982.png?v8", - scorpius:"https://github.githubassets.com/images/icons/emoji/unicode/264f.png?v8", - scotland:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0073-e0063-e0074-e007f.png?v8", - scream:"https://github.githubassets.com/images/icons/emoji/unicode/1f631.png?v8", - scream_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f640.png?v8", - scroll:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dc.png?v8", - seat:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ba.png?v8", - secret:"https://github.githubassets.com/images/icons/emoji/unicode/3299.png?v8", - see_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f648.png?v8", - seedling:"https://github.githubassets.com/images/icons/emoji/unicode/1f331.png?v8", - selfie:"https://github.githubassets.com/images/icons/emoji/unicode/1f933.png?v8", - senegal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f3.png?v8", - serbia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1f8.png?v8", - service_dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f415-1f9ba.png?v8", - seven:"https://github.githubassets.com/images/icons/emoji/unicode/0037-20e3.png?v8", - seychelles:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e8.png?v8", - shallow_pan_of_food:"https://github.githubassets.com/images/icons/emoji/unicode/1f958.png?v8", - shamrock:"https://github.githubassets.com/images/icons/emoji/unicode/2618.png?v8", - shark:"https://github.githubassets.com/images/icons/emoji/unicode/1f988.png?v8", - shaved_ice:"https://github.githubassets.com/images/icons/emoji/unicode/1f367.png?v8", - sheep:"https://github.githubassets.com/images/icons/emoji/unicode/1f411.png?v8", - shell:"https://github.githubassets.com/images/icons/emoji/unicode/1f41a.png?v8", - shield:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e1.png?v8", - shinto_shrine:"https://github.githubassets.com/images/icons/emoji/unicode/26e9.png?v8", - ship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a2.png?v8", - shipit:"https://github.githubassets.com/images/icons/emoji/shipit.png?v8", - shirt:"https://github.githubassets.com/images/icons/emoji/unicode/1f455.png?v8", - shit:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8", - shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45e.png?v8", - shopping:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cd.png?v8", - shopping_cart:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d2.png?v8", - shorts:"https://github.githubassets.com/images/icons/emoji/unicode/1fa73.png?v8", - shower:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bf.png?v8", - shrimp:"https://github.githubassets.com/images/icons/emoji/unicode/1f990.png?v8", - shrug:"https://github.githubassets.com/images/icons/emoji/unicode/1f937.png?v8", - shushing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92b.png?v8", - sierra_leone:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f1.png?v8", - signal_strength:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f6.png?v8", - singapore:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ec.png?v8", - singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3a4.png?v8", - sint_maarten:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fd.png?v8", - six:"https://github.githubassets.com/images/icons/emoji/unicode/0036-20e3.png?v8", - six_pointed_star:"https://github.githubassets.com/images/icons/emoji/unicode/1f52f.png?v8", - skateboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f9.png?v8", - ski:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bf.png?v8", - skier:"https://github.githubassets.com/images/icons/emoji/unicode/26f7.png?v8", - skull:"https://github.githubassets.com/images/icons/emoji/unicode/1f480.png?v8", - skull_and_crossbones:"https://github.githubassets.com/images/icons/emoji/unicode/2620.png?v8", - skunk:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a8.png?v8", - sled:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f7.png?v8", - sleeping:"https://github.githubassets.com/images/icons/emoji/unicode/1f634.png?v8", - sleeping_bed:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cc.png?v8", - sleepy:"https://github.githubassets.com/images/icons/emoji/unicode/1f62a.png?v8", - slightly_frowning_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f641.png?v8", - slightly_smiling_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f642.png?v8", - slot_machine:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b0.png?v8", - sloth:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a5.png?v8", - slovakia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f0.png?v8", - slovenia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ee.png?v8", - small_airplane:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e9.png?v8", - small_blue_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f539.png?v8", - small_orange_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f538.png?v8", - small_red_triangle:"https://github.githubassets.com/images/icons/emoji/unicode/1f53a.png?v8", - small_red_triangle_down:"https://github.githubassets.com/images/icons/emoji/unicode/1f53b.png?v8", - smile:"https://github.githubassets.com/images/icons/emoji/unicode/1f604.png?v8", - smile_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f638.png?v8", - smiley:"https://github.githubassets.com/images/icons/emoji/unicode/1f603.png?v8", - smiley_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63a.png?v8", - smiling_face_with_three_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f970.png?v8", - smiling_imp:"https://github.githubassets.com/images/icons/emoji/unicode/1f608.png?v8", - smirk:"https://github.githubassets.com/images/icons/emoji/unicode/1f60f.png?v8", - smirk_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63c.png?v8", - smoking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ac.png?v8", - snail:"https://github.githubassets.com/images/icons/emoji/unicode/1f40c.png?v8", - snake:"https://github.githubassets.com/images/icons/emoji/unicode/1f40d.png?v8", - sneezing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f927.png?v8", - snowboarder:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c2.png?v8", - snowflake:"https://github.githubassets.com/images/icons/emoji/unicode/2744.png?v8", - snowman:"https://github.githubassets.com/images/icons/emoji/unicode/26c4.png?v8", - snowman_with_snow:"https://github.githubassets.com/images/icons/emoji/unicode/2603.png?v8", - soap:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fc.png?v8", - sob:"https://github.githubassets.com/images/icons/emoji/unicode/1f62d.png?v8", - soccer:"https://github.githubassets.com/images/icons/emoji/unicode/26bd.png?v8", - socks:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e6.png?v8", - softball:"https://github.githubassets.com/images/icons/emoji/unicode/1f94e.png?v8", - solomon_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e7.png?v8", - somalia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f4.png?v8", - soon:"https://github.githubassets.com/images/icons/emoji/unicode/1f51c.png?v8", - sos:"https://github.githubassets.com/images/icons/emoji/unicode/1f198.png?v8", - sound:"https://github.githubassets.com/images/icons/emoji/unicode/1f509.png?v8", - south_africa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1e6.png?v8", - south_georgia_south_sandwich_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f8.png?v8", - south_sudan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f8.png?v8", - space_invader:"https://github.githubassets.com/images/icons/emoji/unicode/1f47e.png?v8", - spades:"https://github.githubassets.com/images/icons/emoji/unicode/2660.png?v8", - spaghetti:"https://github.githubassets.com/images/icons/emoji/unicode/1f35d.png?v8", - sparkle:"https://github.githubassets.com/images/icons/emoji/unicode/2747.png?v8", - sparkler:"https://github.githubassets.com/images/icons/emoji/unicode/1f387.png?v8", - sparkles:"https://github.githubassets.com/images/icons/emoji/unicode/2728.png?v8", - sparkling_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f496.png?v8", - speak_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f64a.png?v8", - speaker:"https://github.githubassets.com/images/icons/emoji/unicode/1f508.png?v8", - speaking_head:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e3.png?v8", - speech_balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ac.png?v8", - speedboat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a4.png?v8", - spider:"https://github.githubassets.com/images/icons/emoji/unicode/1f577.png?v8", - spider_web:"https://github.githubassets.com/images/icons/emoji/unicode/1f578.png?v8", - spiral_calendar:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d3.png?v8", - spiral_notepad:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d2.png?v8", - sponge:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fd.png?v8", - spoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f944.png?v8", - squid:"https://github.githubassets.com/images/icons/emoji/unicode/1f991.png?v8", - sri_lanka:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f0.png?v8", - st_barthelemy:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f1.png?v8", - st_helena:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ed.png?v8", - st_kitts_nevis:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f3.png?v8", - st_lucia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e8.png?v8", - st_martin:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1eb.png?v8", - st_pierre_miquelon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f2.png?v8", - st_vincent_grenadines:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1e8.png?v8", - stadium:"https://github.githubassets.com/images/icons/emoji/unicode/1f3df.png?v8", - standing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd-2642.png?v8", - standing_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd.png?v8", - standing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd-2640.png?v8", - star:"https://github.githubassets.com/images/icons/emoji/unicode/2b50.png?v8", - star2:"https://github.githubassets.com/images/icons/emoji/unicode/1f31f.png?v8", - star_and_crescent:"https://github.githubassets.com/images/icons/emoji/unicode/262a.png?v8", - star_of_david:"https://github.githubassets.com/images/icons/emoji/unicode/2721.png?v8", - star_struck:"https://github.githubassets.com/images/icons/emoji/unicode/1f929.png?v8", - stars:"https://github.githubassets.com/images/icons/emoji/unicode/1f320.png?v8", - station:"https://github.githubassets.com/images/icons/emoji/unicode/1f689.png?v8", - statue_of_liberty:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fd.png?v8", - steam_locomotive:"https://github.githubassets.com/images/icons/emoji/unicode/1f682.png?v8", - stethoscope:"https://github.githubassets.com/images/icons/emoji/unicode/1fa7a.png?v8", - stew:"https://github.githubassets.com/images/icons/emoji/unicode/1f372.png?v8", - stop_button:"https://github.githubassets.com/images/icons/emoji/unicode/23f9.png?v8", - stop_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d1.png?v8", - stopwatch:"https://github.githubassets.com/images/icons/emoji/unicode/23f1.png?v8", - straight_ruler:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cf.png?v8", - strawberry:"https://github.githubassets.com/images/icons/emoji/unicode/1f353.png?v8", - stuck_out_tongue:"https://github.githubassets.com/images/icons/emoji/unicode/1f61b.png?v8", - stuck_out_tongue_closed_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f61d.png?v8", - stuck_out_tongue_winking_eye:"https://github.githubassets.com/images/icons/emoji/unicode/1f61c.png?v8", - student:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f393.png?v8", - studio_microphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f399.png?v8", - stuffed_flatbread:"https://github.githubassets.com/images/icons/emoji/unicode/1f959.png?v8", - sudan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e9.png?v8", - sun_behind_large_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f325.png?v8", - sun_behind_rain_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f326.png?v8", - sun_behind_small_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f324.png?v8", - sun_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31e.png?v8", - sunflower:"https://github.githubassets.com/images/icons/emoji/unicode/1f33b.png?v8", - sunglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f60e.png?v8", - sunny:"https://github.githubassets.com/images/icons/emoji/unicode/2600.png?v8", - sunrise:"https://github.githubassets.com/images/icons/emoji/unicode/1f305.png?v8", - sunrise_over_mountains:"https://github.githubassets.com/images/icons/emoji/unicode/1f304.png?v8", - superhero:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8.png?v8", - superhero_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8-2642.png?v8", - superhero_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8-2640.png?v8", - supervillain:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9.png?v8", - supervillain_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9-2642.png?v8", - supervillain_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9-2640.png?v8", - surfer:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4.png?v8", - surfing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4-2642.png?v8", - surfing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4-2640.png?v8", - suriname:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f7.png?v8", - sushi:"https://github.githubassets.com/images/icons/emoji/unicode/1f363.png?v8", - suspect:"https://github.githubassets.com/images/icons/emoji/suspect.png?v8", - suspension_railway:"https://github.githubassets.com/images/icons/emoji/unicode/1f69f.png?v8", - svalbard_jan_mayen:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ef.png?v8", - swan:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a2.png?v8", - swaziland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ff.png?v8", - sweat:"https://github.githubassets.com/images/icons/emoji/unicode/1f613.png?v8", - sweat_drops:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a6.png?v8", - sweat_smile:"https://github.githubassets.com/images/icons/emoji/unicode/1f605.png?v8", - sweden:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ea.png?v8", - sweet_potato:"https://github.githubassets.com/images/icons/emoji/unicode/1f360.png?v8", - swim_brief:"https://github.githubassets.com/images/icons/emoji/unicode/1fa72.png?v8", - swimmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca.png?v8", - swimming_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca-2642.png?v8", - swimming_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca-2640.png?v8", - switzerland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ed.png?v8", - symbols:"https://github.githubassets.com/images/icons/emoji/unicode/1f523.png?v8", - synagogue:"https://github.githubassets.com/images/icons/emoji/unicode/1f54d.png?v8", - syria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fe.png?v8", - syringe:"https://github.githubassets.com/images/icons/emoji/unicode/1f489.png?v8", - "t-rex":"https://github.githubassets.com/images/icons/emoji/unicode/1f996.png?v8", - taco:"https://github.githubassets.com/images/icons/emoji/unicode/1f32e.png?v8", - tada:"https://github.githubassets.com/images/icons/emoji/unicode/1f389.png?v8", - taiwan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1fc.png?v8", - tajikistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ef.png?v8", - takeout_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f961.png?v8", - tanabata_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f38b.png?v8", - tangerine:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8", - tanzania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ff.png?v8", - taurus:"https://github.githubassets.com/images/icons/emoji/unicode/2649.png?v8", - taxi:"https://github.githubassets.com/images/icons/emoji/unicode/1f695.png?v8", - tea:"https://github.githubassets.com/images/icons/emoji/unicode/1f375.png?v8", - teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3eb.png?v8", - technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f4bb.png?v8", - teddy_bear:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f8.png?v8", - telephone:"https://github.githubassets.com/images/icons/emoji/unicode/260e.png?v8", - telephone_receiver:"https://github.githubassets.com/images/icons/emoji/unicode/1f4de.png?v8", - telescope:"https://github.githubassets.com/images/icons/emoji/unicode/1f52d.png?v8", - tennis:"https://github.githubassets.com/images/icons/emoji/unicode/1f3be.png?v8", - tent:"https://github.githubassets.com/images/icons/emoji/unicode/26fa.png?v8", - test_tube:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ea.png?v8", - thailand:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ed.png?v8", - thermometer:"https://github.githubassets.com/images/icons/emoji/unicode/1f321.png?v8", - thinking:"https://github.githubassets.com/images/icons/emoji/unicode/1f914.png?v8", - thought_balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ad.png?v8", - thread:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f5.png?v8", - three:"https://github.githubassets.com/images/icons/emoji/unicode/0033-20e3.png?v8", - thumbsdown:"https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8", - thumbsup:"https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8", - ticket:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ab.png?v8", - tickets:"https://github.githubassets.com/images/icons/emoji/unicode/1f39f.png?v8", - tiger:"https://github.githubassets.com/images/icons/emoji/unicode/1f42f.png?v8", - tiger2:"https://github.githubassets.com/images/icons/emoji/unicode/1f405.png?v8", - timer_clock:"https://github.githubassets.com/images/icons/emoji/unicode/23f2.png?v8", - timor_leste:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f1.png?v8", - tipping_hand_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2642.png?v8", - tipping_hand_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f481.png?v8", - tipping_hand_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2640.png?v8", - tired_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f62b.png?v8", - tm:"https://github.githubassets.com/images/icons/emoji/unicode/2122.png?v8", - togo:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ec.png?v8", - toilet:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bd.png?v8", - tokelau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f0.png?v8", - tokyo_tower:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fc.png?v8", - tomato:"https://github.githubassets.com/images/icons/emoji/unicode/1f345.png?v8", - tonga:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f4.png?v8", - tongue:"https://github.githubassets.com/images/icons/emoji/unicode/1f445.png?v8", - toolbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f0.png?v8", - tooth:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b7.png?v8", - top:"https://github.githubassets.com/images/icons/emoji/unicode/1f51d.png?v8", - tophat:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a9.png?v8", - tornado:"https://github.githubassets.com/images/icons/emoji/unicode/1f32a.png?v8", - tr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f7.png?v8", - trackball:"https://github.githubassets.com/images/icons/emoji/unicode/1f5b2.png?v8", - tractor:"https://github.githubassets.com/images/icons/emoji/unicode/1f69c.png?v8", - traffic_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a5.png?v8", - train:"https://github.githubassets.com/images/icons/emoji/unicode/1f68b.png?v8", - train2:"https://github.githubassets.com/images/icons/emoji/unicode/1f686.png?v8", - tram:"https://github.githubassets.com/images/icons/emoji/unicode/1f68a.png?v8", - triangular_flag_on_post:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a9.png?v8", - triangular_ruler:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d0.png?v8", - trident:"https://github.githubassets.com/images/icons/emoji/unicode/1f531.png?v8", - trinidad_tobago:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f9.png?v8", - tristan_da_cunha:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e6.png?v8", - triumph:"https://github.githubassets.com/images/icons/emoji/unicode/1f624.png?v8", - trolleybus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68e.png?v8", - trollface:"https://github.githubassets.com/images/icons/emoji/trollface.png?v8", - trophy:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c6.png?v8", - tropical_drink:"https://github.githubassets.com/images/icons/emoji/unicode/1f379.png?v8", - tropical_fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f420.png?v8", - truck:"https://github.githubassets.com/images/icons/emoji/unicode/1f69a.png?v8", - trumpet:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ba.png?v8", - tshirt:"https://github.githubassets.com/images/icons/emoji/unicode/1f455.png?v8", - tulip:"https://github.githubassets.com/images/icons/emoji/unicode/1f337.png?v8", - tumbler_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f943.png?v8", - tunisia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f3.png?v8", - turkey:"https://github.githubassets.com/images/icons/emoji/unicode/1f983.png?v8", - turkmenistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f2.png?v8", - turks_caicos_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e8.png?v8", - turtle:"https://github.githubassets.com/images/icons/emoji/unicode/1f422.png?v8", - tuvalu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1fb.png?v8", - tv:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fa.png?v8", - twisted_rightwards_arrows:"https://github.githubassets.com/images/icons/emoji/unicode/1f500.png?v8", - two:"https://github.githubassets.com/images/icons/emoji/unicode/0032-20e3.png?v8", - two_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f495.png?v8", - two_men_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f46c.png?v8", - two_women_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f46d.png?v8", - u5272:"https://github.githubassets.com/images/icons/emoji/unicode/1f239.png?v8", - u5408:"https://github.githubassets.com/images/icons/emoji/unicode/1f234.png?v8", - u55b6:"https://github.githubassets.com/images/icons/emoji/unicode/1f23a.png?v8", - u6307:"https://github.githubassets.com/images/icons/emoji/unicode/1f22f.png?v8", - u6708:"https://github.githubassets.com/images/icons/emoji/unicode/1f237.png?v8", - u6709:"https://github.githubassets.com/images/icons/emoji/unicode/1f236.png?v8", - u6e80:"https://github.githubassets.com/images/icons/emoji/unicode/1f235.png?v8", - u7121:"https://github.githubassets.com/images/icons/emoji/unicode/1f21a.png?v8", - u7533:"https://github.githubassets.com/images/icons/emoji/unicode/1f238.png?v8", - u7981:"https://github.githubassets.com/images/icons/emoji/unicode/1f232.png?v8", - u7a7a:"https://github.githubassets.com/images/icons/emoji/unicode/1f233.png?v8", - uganda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1ec.png?v8", - uk:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e7.png?v8", - ukraine:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1e6.png?v8", - umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/2614.png?v8", - unamused:"https://github.githubassets.com/images/icons/emoji/unicode/1f612.png?v8", - underage:"https://github.githubassets.com/images/icons/emoji/unicode/1f51e.png?v8", - unicorn:"https://github.githubassets.com/images/icons/emoji/unicode/1f984.png?v8", - united_arab_emirates:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ea.png?v8", - united_nations:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f3.png?v8", - unlock:"https://github.githubassets.com/images/icons/emoji/unicode/1f513.png?v8", - up:"https://github.githubassets.com/images/icons/emoji/unicode/1f199.png?v8", - upside_down_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f643.png?v8", - uruguay:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1fe.png?v8", - us:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f8.png?v8", - us_outlying_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f2.png?v8", - us_virgin_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ee.png?v8", - uzbekistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1ff.png?v8", - v:"https://github.githubassets.com/images/icons/emoji/unicode/270c.png?v8", - vampire:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db.png?v8", - vampire_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db-2642.png?v8", - vampire_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db-2640.png?v8", - vanuatu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1fa.png?v8", - vatican_city:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1e6.png?v8", - venezuela:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ea.png?v8", - vertical_traffic_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a6.png?v8", - vhs:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fc.png?v8", - vibration_mode:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f3.png?v8", - video_camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f9.png?v8", - video_game:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ae.png?v8", - vietnam:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1f3.png?v8", - violin:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bb.png?v8", - virgo:"https://github.githubassets.com/images/icons/emoji/unicode/264d.png?v8", - volcano:"https://github.githubassets.com/images/icons/emoji/unicode/1f30b.png?v8", - volleyball:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d0.png?v8", - vomiting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92e.png?v8", - vs:"https://github.githubassets.com/images/icons/emoji/unicode/1f19a.png?v8", - vulcan_salute:"https://github.githubassets.com/images/icons/emoji/unicode/1f596.png?v8", - waffle:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c7.png?v8", - wales:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0077-e006c-e0073-e007f.png?v8", - walking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6.png?v8", - walking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6-2642.png?v8", - walking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6-2640.png?v8", - wallis_futuna:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fc-1f1eb.png?v8", - waning_crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f318.png?v8", - waning_gibbous_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f316.png?v8", - warning:"https://github.githubassets.com/images/icons/emoji/unicode/26a0.png?v8", - wastebasket:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d1.png?v8", - watch:"https://github.githubassets.com/images/icons/emoji/unicode/231a.png?v8", - water_buffalo:"https://github.githubassets.com/images/icons/emoji/unicode/1f403.png?v8", - water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d.png?v8", - watermelon:"https://github.githubassets.com/images/icons/emoji/unicode/1f349.png?v8", - wave:"https://github.githubassets.com/images/icons/emoji/unicode/1f44b.png?v8", - wavy_dash:"https://github.githubassets.com/images/icons/emoji/unicode/3030.png?v8", - waxing_crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f312.png?v8", - waxing_gibbous_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f314.png?v8", - wc:"https://github.githubassets.com/images/icons/emoji/unicode/1f6be.png?v8", - weary:"https://github.githubassets.com/images/icons/emoji/unicode/1f629.png?v8", - wedding:"https://github.githubassets.com/images/icons/emoji/unicode/1f492.png?v8", - weight_lifting:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb.png?v8", - weight_lifting_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb-2642.png?v8", - weight_lifting_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb-2640.png?v8", - western_sahara:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ed.png?v8", - whale:"https://github.githubassets.com/images/icons/emoji/unicode/1f433.png?v8", - whale2:"https://github.githubassets.com/images/icons/emoji/unicode/1f40b.png?v8", - wheel_of_dharma:"https://github.githubassets.com/images/icons/emoji/unicode/2638.png?v8", - wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/267f.png?v8", - white_check_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2705.png?v8", - white_circle:"https://github.githubassets.com/images/icons/emoji/unicode/26aa.png?v8", - white_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f3.png?v8", - white_flower:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ae.png?v8", - white_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b3.png?v8", - white_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b3.png?v8", - white_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f90d.png?v8", - white_large_square:"https://github.githubassets.com/images/icons/emoji/unicode/2b1c.png?v8", - white_medium_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fd.png?v8", - white_medium_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fb.png?v8", - white_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25ab.png?v8", - white_square_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f533.png?v8", - wilted_flower:"https://github.githubassets.com/images/icons/emoji/unicode/1f940.png?v8", - wind_chime:"https://github.githubassets.com/images/icons/emoji/unicode/1f390.png?v8", - wind_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f32c.png?v8", - wine_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f377.png?v8", - wink:"https://github.githubassets.com/images/icons/emoji/unicode/1f609.png?v8", - wolf:"https://github.githubassets.com/images/icons/emoji/unicode/1f43a.png?v8", - woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469.png?v8", - woman_artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3a8.png?v8", - woman_astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f680.png?v8", - woman_cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938-2640.png?v8", - woman_cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f373.png?v8", - woman_dancing:"https://github.githubassets.com/images/icons/emoji/unicode/1f483.png?v8", - woman_facepalming:"https://github.githubassets.com/images/icons/emoji/unicode/1f926-2640.png?v8", - woman_factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3ed.png?v8", - woman_farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f33e.png?v8", - woman_firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f692.png?v8", - woman_health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2695.png?v8", - woman_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9bd.png?v8", - woman_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9bc.png?v8", - woman_judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2696.png?v8", - woman_juggling:"https://github.githubassets.com/images/icons/emoji/unicode/1f939-2640.png?v8", - woman_mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f527.png?v8", - woman_office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f4bc.png?v8", - woman_pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2708.png?v8", - woman_playing_handball:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e-2640.png?v8", - woman_playing_water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d-2640.png?v8", - woman_scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f52c.png?v8", - woman_shrugging:"https://github.githubassets.com/images/icons/emoji/unicode/1f937-2640.png?v8", - woman_singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3a4.png?v8", - woman_student:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f393.png?v8", - woman_teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3eb.png?v8", - woman_technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f4bb.png?v8", - woman_with_headscarf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d5.png?v8", - woman_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9af.png?v8", - woman_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473-2640.png?v8", - womans_clothes:"https://github.githubassets.com/images/icons/emoji/unicode/1f45a.png?v8", - womans_hat:"https://github.githubassets.com/images/icons/emoji/unicode/1f452.png?v8", - women_wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c-2640.png?v8", - womens:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ba.png?v8", - woozy_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f974.png?v8", - world_map:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fa.png?v8", - worried:"https://github.githubassets.com/images/icons/emoji/unicode/1f61f.png?v8", - wrench:"https://github.githubassets.com/images/icons/emoji/unicode/1f527.png?v8", - wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c.png?v8", - writing_hand:"https://github.githubassets.com/images/icons/emoji/unicode/270d.png?v8", - x:"https://github.githubassets.com/images/icons/emoji/unicode/274c.png?v8", - yarn:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f6.png?v8", - yawning_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f971.png?v8", - yellow_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e1.png?v8", - yellow_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49b.png?v8", - yellow_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e8.png?v8", - yemen:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fe-1f1ea.png?v8", - yen:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b4.png?v8", - yin_yang:"https://github.githubassets.com/images/icons/emoji/unicode/262f.png?v8", - yo_yo:"https://github.githubassets.com/images/icons/emoji/unicode/1fa80.png?v8", - yum:"https://github.githubassets.com/images/icons/emoji/unicode/1f60b.png?v8", - zambia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1f2.png?v8", - zany_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92a.png?v8", - zap:"https://github.githubassets.com/images/icons/emoji/unicode/26a1.png?v8", - zebra:"https://github.githubassets.com/images/icons/emoji/unicode/1f993.png?v8", - zero:"https://github.githubassets.com/images/icons/emoji/unicode/0030-20e3.png?v8", - zimbabwe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1fc.png?v8", - zipper_mouth_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f910.png?v8", - zombie:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df.png?v8", - zombie_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df-2642.png?v8", - zombie_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df-2640.png?v8", - zzz:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a4.png?v8", - }, - }) - } +export default function({faker}, target, that) { + console.debug("metrics/compute/mocks > mocking rest api result > rest.emojis.get") + return ({ + status:200, + url:"https://api.github.com/emojis", + headers:{ + server:"GitHub.com", + status:"200 OK", + "x-oauth-scopes":"repo", + }, + data:{ + "100":"https://github.githubassets.com/images/icons/emoji/unicode/1f4af.png?v8", + "1234":"https://github.githubassets.com/images/icons/emoji/unicode/1f522.png?v8", + "+1":"https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8", + "-1":"https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8", + "1st_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f947.png?v8", + "2nd_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f948.png?v8", + "3rd_place_medal":"https://github.githubassets.com/images/icons/emoji/unicode/1f949.png?v8", + "8ball":"https://github.githubassets.com/images/icons/emoji/unicode/1f3b1.png?v8", + a:"https://github.githubassets.com/images/icons/emoji/unicode/1f170.png?v8", + ab:"https://github.githubassets.com/images/icons/emoji/unicode/1f18e.png?v8", + abacus:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ee.png?v8", + abc:"https://github.githubassets.com/images/icons/emoji/unicode/1f524.png?v8", + abcd:"https://github.githubassets.com/images/icons/emoji/unicode/1f521.png?v8", + accept:"https://github.githubassets.com/images/icons/emoji/unicode/1f251.png?v8", + adhesive_bandage:"https://github.githubassets.com/images/icons/emoji/unicode/1fa79.png?v8", + adult:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1.png?v8", + aerial_tramway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a1.png?v8", + afghanistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1eb.png?v8", + airplane:"https://github.githubassets.com/images/icons/emoji/unicode/2708.png?v8", + aland_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fd.png?v8", + alarm_clock:"https://github.githubassets.com/images/icons/emoji/unicode/23f0.png?v8", + albania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f1.png?v8", + alembic:"https://github.githubassets.com/images/icons/emoji/unicode/2697.png?v8", + algeria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ff.png?v8", + alien:"https://github.githubassets.com/images/icons/emoji/unicode/1f47d.png?v8", + ambulance:"https://github.githubassets.com/images/icons/emoji/unicode/1f691.png?v8", + american_samoa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f8.png?v8", + amphora:"https://github.githubassets.com/images/icons/emoji/unicode/1f3fa.png?v8", + anchor:"https://github.githubassets.com/images/icons/emoji/unicode/2693.png?v8", + andorra:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1e9.png?v8", + angel:"https://github.githubassets.com/images/icons/emoji/unicode/1f47c.png?v8", + anger:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a2.png?v8", + angola:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f4.png?v8", + angry:"https://github.githubassets.com/images/icons/emoji/unicode/1f620.png?v8", + anguilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ee.png?v8", + anguished:"https://github.githubassets.com/images/icons/emoji/unicode/1f627.png?v8", + ant:"https://github.githubassets.com/images/icons/emoji/unicode/1f41c.png?v8", + antarctica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f6.png?v8", + antigua_barbuda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ec.png?v8", + apple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34e.png?v8", + aquarius:"https://github.githubassets.com/images/icons/emoji/unicode/2652.png?v8", + argentina:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f7.png?v8", + aries:"https://github.githubassets.com/images/icons/emoji/unicode/2648.png?v8", + armenia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f2.png?v8", + arrow_backward:"https://github.githubassets.com/images/icons/emoji/unicode/25c0.png?v8", + arrow_double_down:"https://github.githubassets.com/images/icons/emoji/unicode/23ec.png?v8", + arrow_double_up:"https://github.githubassets.com/images/icons/emoji/unicode/23eb.png?v8", + arrow_down:"https://github.githubassets.com/images/icons/emoji/unicode/2b07.png?v8", + arrow_down_small:"https://github.githubassets.com/images/icons/emoji/unicode/1f53d.png?v8", + arrow_forward:"https://github.githubassets.com/images/icons/emoji/unicode/25b6.png?v8", + arrow_heading_down:"https://github.githubassets.com/images/icons/emoji/unicode/2935.png?v8", + arrow_heading_up:"https://github.githubassets.com/images/icons/emoji/unicode/2934.png?v8", + arrow_left:"https://github.githubassets.com/images/icons/emoji/unicode/2b05.png?v8", + arrow_lower_left:"https://github.githubassets.com/images/icons/emoji/unicode/2199.png?v8", + arrow_lower_right:"https://github.githubassets.com/images/icons/emoji/unicode/2198.png?v8", + arrow_right:"https://github.githubassets.com/images/icons/emoji/unicode/27a1.png?v8", + arrow_right_hook:"https://github.githubassets.com/images/icons/emoji/unicode/21aa.png?v8", + arrow_up:"https://github.githubassets.com/images/icons/emoji/unicode/2b06.png?v8", + arrow_up_down:"https://github.githubassets.com/images/icons/emoji/unicode/2195.png?v8", + arrow_up_small:"https://github.githubassets.com/images/icons/emoji/unicode/1f53c.png?v8", + arrow_upper_left:"https://github.githubassets.com/images/icons/emoji/unicode/2196.png?v8", + arrow_upper_right:"https://github.githubassets.com/images/icons/emoji/unicode/2197.png?v8", + arrows_clockwise:"https://github.githubassets.com/images/icons/emoji/unicode/1f503.png?v8", + arrows_counterclockwise:"https://github.githubassets.com/images/icons/emoji/unicode/1f504.png?v8", + art:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a8.png?v8", + articulated_lorry:"https://github.githubassets.com/images/icons/emoji/unicode/1f69b.png?v8", + artificial_satellite:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f0.png?v8", + artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3a8.png?v8", + aruba:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fc.png?v8", + ascension_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1e8.png?v8", + asterisk:"https://github.githubassets.com/images/icons/emoji/unicode/002a-20e3.png?v8", + astonished:"https://github.githubassets.com/images/icons/emoji/unicode/1f632.png?v8", + astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f680.png?v8", + athletic_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45f.png?v8", + atm:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e7.png?v8", + atom:"https://github.githubassets.com/images/icons/emoji/atom.png?v8", + atom_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/269b.png?v8", + australia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1fa.png?v8", + austria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1f9.png?v8", + auto_rickshaw:"https://github.githubassets.com/images/icons/emoji/unicode/1f6fa.png?v8", + avocado:"https://github.githubassets.com/images/icons/emoji/unicode/1f951.png?v8", + axe:"https://github.githubassets.com/images/icons/emoji/unicode/1fa93.png?v8", + azerbaijan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ff.png?v8", + b:"https://github.githubassets.com/images/icons/emoji/unicode/1f171.png?v8", + baby:"https://github.githubassets.com/images/icons/emoji/unicode/1f476.png?v8", + baby_bottle:"https://github.githubassets.com/images/icons/emoji/unicode/1f37c.png?v8", + baby_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f424.png?v8", + baby_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bc.png?v8", + back:"https://github.githubassets.com/images/icons/emoji/unicode/1f519.png?v8", + bacon:"https://github.githubassets.com/images/icons/emoji/unicode/1f953.png?v8", + badger:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a1.png?v8", + badminton:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f8.png?v8", + bagel:"https://github.githubassets.com/images/icons/emoji/unicode/1f96f.png?v8", + baggage_claim:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c4.png?v8", + baguette_bread:"https://github.githubassets.com/images/icons/emoji/unicode/1f956.png?v8", + bahamas:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f8.png?v8", + bahrain:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ed.png?v8", + balance_scale:"https://github.githubassets.com/images/icons/emoji/unicode/2696.png?v8", + bald_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b2.png?v8", + bald_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b2.png?v8", + ballet_shoes:"https://github.githubassets.com/images/icons/emoji/unicode/1fa70.png?v8", + balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f388.png?v8", + ballot_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f5f3.png?v8", + ballot_box_with_check:"https://github.githubassets.com/images/icons/emoji/unicode/2611.png?v8", + bamboo:"https://github.githubassets.com/images/icons/emoji/unicode/1f38d.png?v8", + banana:"https://github.githubassets.com/images/icons/emoji/unicode/1f34c.png?v8", + bangbang:"https://github.githubassets.com/images/icons/emoji/unicode/203c.png?v8", + bangladesh:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e9.png?v8", + banjo:"https://github.githubassets.com/images/icons/emoji/unicode/1fa95.png?v8", + bank:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e6.png?v8", + bar_chart:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ca.png?v8", + barbados:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e7.png?v8", + barber:"https://github.githubassets.com/images/icons/emoji/unicode/1f488.png?v8", + baseball:"https://github.githubassets.com/images/icons/emoji/unicode/26be.png?v8", + basecamp:"https://github.githubassets.com/images/icons/emoji/basecamp.png?v8", + basecampy:"https://github.githubassets.com/images/icons/emoji/basecampy.png?v8", + basket:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fa.png?v8", + basketball:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c0.png?v8", + basketball_man:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2642.png?v8", + basketball_woman:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2640.png?v8", + bat:"https://github.githubassets.com/images/icons/emoji/unicode/1f987.png?v8", + bath:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c0.png?v8", + bathtub:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c1.png?v8", + battery:"https://github.githubassets.com/images/icons/emoji/unicode/1f50b.png?v8", + beach_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d6.png?v8", + bear:"https://github.githubassets.com/images/icons/emoji/unicode/1f43b.png?v8", + bearded_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d4.png?v8", + bed:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cf.png?v8", + bee:"https://github.githubassets.com/images/icons/emoji/unicode/1f41d.png?v8", + beer:"https://github.githubassets.com/images/icons/emoji/unicode/1f37a.png?v8", + beers:"https://github.githubassets.com/images/icons/emoji/unicode/1f37b.png?v8", + beetle:"https://github.githubassets.com/images/icons/emoji/unicode/1f41e.png?v8", + beginner:"https://github.githubassets.com/images/icons/emoji/unicode/1f530.png?v8", + belarus:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fe.png?v8", + belgium:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ea.png?v8", + belize:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ff.png?v8", + bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f514.png?v8", + bellhop_bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ce.png?v8", + benin:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ef.png?v8", + bento:"https://github.githubassets.com/images/icons/emoji/unicode/1f371.png?v8", + bermuda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f2.png?v8", + beverage_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c3.png?v8", + bhutan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f9.png?v8", + bicyclist:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4.png?v8", + bike:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b2.png?v8", + biking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4-2642.png?v8", + biking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b4-2640.png?v8", + bikini:"https://github.githubassets.com/images/icons/emoji/unicode/1f459.png?v8", + billed_cap:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e2.png?v8", + biohazard:"https://github.githubassets.com/images/icons/emoji/unicode/2623.png?v8", + bird:"https://github.githubassets.com/images/icons/emoji/unicode/1f426.png?v8", + birthday:"https://github.githubassets.com/images/icons/emoji/unicode/1f382.png?v8", + black_circle:"https://github.githubassets.com/images/icons/emoji/unicode/26ab.png?v8", + black_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4.png?v8", + black_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a4.png?v8", + black_joker:"https://github.githubassets.com/images/icons/emoji/unicode/1f0cf.png?v8", + black_large_square:"https://github.githubassets.com/images/icons/emoji/unicode/2b1b.png?v8", + black_medium_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fe.png?v8", + black_medium_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fc.png?v8", + black_nib:"https://github.githubassets.com/images/icons/emoji/unicode/2712.png?v8", + black_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25aa.png?v8", + black_square_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f532.png?v8", + blond_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2642.png?v8", + blond_haired_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f471.png?v8", + blond_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2640.png?v8", + blonde_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f471-2640.png?v8", + blossom:"https://github.githubassets.com/images/icons/emoji/unicode/1f33c.png?v8", + blowfish:"https://github.githubassets.com/images/icons/emoji/unicode/1f421.png?v8", + blue_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d8.png?v8", + blue_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f699.png?v8", + blue_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f499.png?v8", + blue_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e6.png?v8", + blush:"https://github.githubassets.com/images/icons/emoji/unicode/1f60a.png?v8", + boar:"https://github.githubassets.com/images/icons/emoji/unicode/1f417.png?v8", + boat:"https://github.githubassets.com/images/icons/emoji/unicode/26f5.png?v8", + bolivia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f4.png?v8", + bomb:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a3.png?v8", + bone:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b4.png?v8", + book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d6.png?v8", + bookmark:"https://github.githubassets.com/images/icons/emoji/unicode/1f516.png?v8", + bookmark_tabs:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d1.png?v8", + books:"https://github.githubassets.com/images/icons/emoji/unicode/1f4da.png?v8", + boom:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png?v8", + boot:"https://github.githubassets.com/images/icons/emoji/unicode/1f462.png?v8", + bosnia_herzegovina:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1e6.png?v8", + botswana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fc.png?v8", + bouncing_ball_man:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2642.png?v8", + bouncing_ball_person:"https://github.githubassets.com/images/icons/emoji/unicode/26f9.png?v8", + bouncing_ball_woman:"https://github.githubassets.com/images/icons/emoji/unicode/26f9-2640.png?v8", + bouquet:"https://github.githubassets.com/images/icons/emoji/unicode/1f490.png?v8", + bouvet_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1fb.png?v8", + bow:"https://github.githubassets.com/images/icons/emoji/unicode/1f647.png?v8", + bow_and_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f9.png?v8", + bowing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f647-2642.png?v8", + bowing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f647-2640.png?v8", + bowl_with_spoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f963.png?v8", + bowling:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b3.png?v8", + bowtie:"https://github.githubassets.com/images/icons/emoji/bowtie.png?v8", + boxing_glove:"https://github.githubassets.com/images/icons/emoji/unicode/1f94a.png?v8", + boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f466.png?v8", + brain:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e0.png?v8", + brazil:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f7.png?v8", + bread:"https://github.githubassets.com/images/icons/emoji/unicode/1f35e.png?v8", + breast_feeding:"https://github.githubassets.com/images/icons/emoji/unicode/1f931.png?v8", + bricks:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f1.png?v8", + bride_with_veil:"https://github.githubassets.com/images/icons/emoji/unicode/1f470.png?v8", + bridge_at_night:"https://github.githubassets.com/images/icons/emoji/unicode/1f309.png?v8", + briefcase:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bc.png?v8", + british_indian_ocean_territory:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f4.png?v8", + british_virgin_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ec.png?v8", + broccoli:"https://github.githubassets.com/images/icons/emoji/unicode/1f966.png?v8", + broken_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f494.png?v8", + broom:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f9.png?v8", + brown_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e4.png?v8", + brown_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f90e.png?v8", + brown_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7eb.png?v8", + brunei:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f3.png?v8", + bug:"https://github.githubassets.com/images/icons/emoji/unicode/1f41b.png?v8", + building_construction:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d7.png?v8", + bulb:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a1.png?v8", + bulgaria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ec.png?v8", + bullettrain_front:"https://github.githubassets.com/images/icons/emoji/unicode/1f685.png?v8", + bullettrain_side:"https://github.githubassets.com/images/icons/emoji/unicode/1f684.png?v8", + burkina_faso:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1eb.png?v8", + burrito:"https://github.githubassets.com/images/icons/emoji/unicode/1f32f.png?v8", + burundi:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1ee.png?v8", + bus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68c.png?v8", + business_suit_levitating:"https://github.githubassets.com/images/icons/emoji/unicode/1f574.png?v8", + busstop:"https://github.githubassets.com/images/icons/emoji/unicode/1f68f.png?v8", + bust_in_silhouette:"https://github.githubassets.com/images/icons/emoji/unicode/1f464.png?v8", + busts_in_silhouette:"https://github.githubassets.com/images/icons/emoji/unicode/1f465.png?v8", + butter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c8.png?v8", + butterfly:"https://github.githubassets.com/images/icons/emoji/unicode/1f98b.png?v8", + cactus:"https://github.githubassets.com/images/icons/emoji/unicode/1f335.png?v8", + cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f370.png?v8", + calendar:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c6.png?v8", + call_me_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f919.png?v8", + calling:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f2.png?v8", + cambodia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ed.png?v8", + camel:"https://github.githubassets.com/images/icons/emoji/unicode/1f42b.png?v8", + camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f7.png?v8", + camera_flash:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f8.png?v8", + cameroon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f2.png?v8", + camping:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d5.png?v8", + canada:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e6.png?v8", + canary_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1e8.png?v8", + cancer:"https://github.githubassets.com/images/icons/emoji/unicode/264b.png?v8", + candle:"https://github.githubassets.com/images/icons/emoji/unicode/1f56f.png?v8", + candy:"https://github.githubassets.com/images/icons/emoji/unicode/1f36c.png?v8", + canned_food:"https://github.githubassets.com/images/icons/emoji/unicode/1f96b.png?v8", + canoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f6.png?v8", + cape_verde:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fb.png?v8", + capital_abcd:"https://github.githubassets.com/images/icons/emoji/unicode/1f520.png?v8", + capricorn:"https://github.githubassets.com/images/icons/emoji/unicode/2651.png?v8", + car:"https://github.githubassets.com/images/icons/emoji/unicode/1f697.png?v8", + card_file_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c3.png?v8", + card_index:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c7.png?v8", + card_index_dividers:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c2.png?v8", + caribbean_netherlands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f6.png?v8", + carousel_horse:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a0.png?v8", + carrot:"https://github.githubassets.com/images/icons/emoji/unicode/1f955.png?v8", + cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938.png?v8", + cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f431.png?v8", + cat2:"https://github.githubassets.com/images/icons/emoji/unicode/1f408.png?v8", + cayman_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1fe.png?v8", + cd:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bf.png?v8", + central_african_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1eb.png?v8", + ceuta_melilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1e6.png?v8", + chad:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e9.png?v8", + chains:"https://github.githubassets.com/images/icons/emoji/unicode/26d3.png?v8", + chair:"https://github.githubassets.com/images/icons/emoji/unicode/1fa91.png?v8", + champagne:"https://github.githubassets.com/images/icons/emoji/unicode/1f37e.png?v8", + chart:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b9.png?v8", + chart_with_downwards_trend:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c9.png?v8", + chart_with_upwards_trend:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c8.png?v8", + checkered_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c1.png?v8", + cheese:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c0.png?v8", + cherries:"https://github.githubassets.com/images/icons/emoji/unicode/1f352.png?v8", + cherry_blossom:"https://github.githubassets.com/images/icons/emoji/unicode/1f338.png?v8", + chess_pawn:"https://github.githubassets.com/images/icons/emoji/unicode/265f.png?v8", + chestnut:"https://github.githubassets.com/images/icons/emoji/unicode/1f330.png?v8", + chicken:"https://github.githubassets.com/images/icons/emoji/unicode/1f414.png?v8", + child:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d2.png?v8", + children_crossing:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b8.png?v8", + chile:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f1.png?v8", + chipmunk:"https://github.githubassets.com/images/icons/emoji/unicode/1f43f.png?v8", + chocolate_bar:"https://github.githubassets.com/images/icons/emoji/unicode/1f36b.png?v8", + chopsticks:"https://github.githubassets.com/images/icons/emoji/unicode/1f962.png?v8", + christmas_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fd.png?v8", + christmas_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f384.png?v8", + church:"https://github.githubassets.com/images/icons/emoji/unicode/26ea.png?v8", + cinema:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a6.png?v8", + circus_tent:"https://github.githubassets.com/images/icons/emoji/unicode/1f3aa.png?v8", + city_sunrise:"https://github.githubassets.com/images/icons/emoji/unicode/1f307.png?v8", + city_sunset:"https://github.githubassets.com/images/icons/emoji/unicode/1f306.png?v8", + cityscape:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d9.png?v8", + cl:"https://github.githubassets.com/images/icons/emoji/unicode/1f191.png?v8", + clamp:"https://github.githubassets.com/images/icons/emoji/unicode/1f5dc.png?v8", + clap:"https://github.githubassets.com/images/icons/emoji/unicode/1f44f.png?v8", + clapper:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ac.png?v8", + classical_building:"https://github.githubassets.com/images/icons/emoji/unicode/1f3db.png?v8", + climbing:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7.png?v8", + climbing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7-2642.png?v8", + climbing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d7-2640.png?v8", + clinking_glasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f942.png?v8", + clipboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cb.png?v8", + clipperton_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f5.png?v8", + clock1:"https://github.githubassets.com/images/icons/emoji/unicode/1f550.png?v8", + clock10:"https://github.githubassets.com/images/icons/emoji/unicode/1f559.png?v8", + clock1030:"https://github.githubassets.com/images/icons/emoji/unicode/1f565.png?v8", + clock11:"https://github.githubassets.com/images/icons/emoji/unicode/1f55a.png?v8", + clock1130:"https://github.githubassets.com/images/icons/emoji/unicode/1f566.png?v8", + clock12:"https://github.githubassets.com/images/icons/emoji/unicode/1f55b.png?v8", + clock1230:"https://github.githubassets.com/images/icons/emoji/unicode/1f567.png?v8", + clock130:"https://github.githubassets.com/images/icons/emoji/unicode/1f55c.png?v8", + clock2:"https://github.githubassets.com/images/icons/emoji/unicode/1f551.png?v8", + clock230:"https://github.githubassets.com/images/icons/emoji/unicode/1f55d.png?v8", + clock3:"https://github.githubassets.com/images/icons/emoji/unicode/1f552.png?v8", + clock330:"https://github.githubassets.com/images/icons/emoji/unicode/1f55e.png?v8", + clock4:"https://github.githubassets.com/images/icons/emoji/unicode/1f553.png?v8", + clock430:"https://github.githubassets.com/images/icons/emoji/unicode/1f55f.png?v8", + clock5:"https://github.githubassets.com/images/icons/emoji/unicode/1f554.png?v8", + clock530:"https://github.githubassets.com/images/icons/emoji/unicode/1f560.png?v8", + clock6:"https://github.githubassets.com/images/icons/emoji/unicode/1f555.png?v8", + clock630:"https://github.githubassets.com/images/icons/emoji/unicode/1f561.png?v8", + clock7:"https://github.githubassets.com/images/icons/emoji/unicode/1f556.png?v8", + clock730:"https://github.githubassets.com/images/icons/emoji/unicode/1f562.png?v8", + clock8:"https://github.githubassets.com/images/icons/emoji/unicode/1f557.png?v8", + clock830:"https://github.githubassets.com/images/icons/emoji/unicode/1f563.png?v8", + clock9:"https://github.githubassets.com/images/icons/emoji/unicode/1f558.png?v8", + clock930:"https://github.githubassets.com/images/icons/emoji/unicode/1f564.png?v8", + closed_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d5.png?v8", + closed_lock_with_key:"https://github.githubassets.com/images/icons/emoji/unicode/1f510.png?v8", + closed_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/1f302.png?v8", + cloud:"https://github.githubassets.com/images/icons/emoji/unicode/2601.png?v8", + cloud_with_lightning:"https://github.githubassets.com/images/icons/emoji/unicode/1f329.png?v8", + cloud_with_lightning_and_rain:"https://github.githubassets.com/images/icons/emoji/unicode/26c8.png?v8", + cloud_with_rain:"https://github.githubassets.com/images/icons/emoji/unicode/1f327.png?v8", + cloud_with_snow:"https://github.githubassets.com/images/icons/emoji/unicode/1f328.png?v8", + clown_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f921.png?v8", + clubs:"https://github.githubassets.com/images/icons/emoji/unicode/2663.png?v8", + cn:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f3.png?v8", + coat:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e5.png?v8", + cocktail:"https://github.githubassets.com/images/icons/emoji/unicode/1f378.png?v8", + coconut:"https://github.githubassets.com/images/icons/emoji/unicode/1f965.png?v8", + cocos_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e8.png?v8", + coffee:"https://github.githubassets.com/images/icons/emoji/unicode/2615.png?v8", + coffin:"https://github.githubassets.com/images/icons/emoji/unicode/26b0.png?v8", + cold_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f976.png?v8", + cold_sweat:"https://github.githubassets.com/images/icons/emoji/unicode/1f630.png?v8", + collision:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png?v8", + colombia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f4.png?v8", + comet:"https://github.githubassets.com/images/icons/emoji/unicode/2604.png?v8", + comoros:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f2.png?v8", + compass:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ed.png?v8", + computer:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bb.png?v8", + computer_mouse:"https://github.githubassets.com/images/icons/emoji/unicode/1f5b1.png?v8", + confetti_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f38a.png?v8", + confounded:"https://github.githubassets.com/images/icons/emoji/unicode/1f616.png?v8", + confused:"https://github.githubassets.com/images/icons/emoji/unicode/1f615.png?v8", + congo_brazzaville:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ec.png?v8", + congo_kinshasa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1e9.png?v8", + congratulations:"https://github.githubassets.com/images/icons/emoji/unicode/3297.png?v8", + construction:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a7.png?v8", + construction_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f477.png?v8", + construction_worker_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f477-2642.png?v8", + construction_worker_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f477-2640.png?v8", + control_knobs:"https://github.githubassets.com/images/icons/emoji/unicode/1f39b.png?v8", + convenience_store:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ea.png?v8", + cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f373.png?v8", + cook_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f0.png?v8", + cookie:"https://github.githubassets.com/images/icons/emoji/unicode/1f36a.png?v8", + cool:"https://github.githubassets.com/images/icons/emoji/unicode/1f192.png?v8", + cop:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e.png?v8", + copyright:"https://github.githubassets.com/images/icons/emoji/unicode/00a9.png?v8", + corn:"https://github.githubassets.com/images/icons/emoji/unicode/1f33d.png?v8", + costa_rica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1f7.png?v8", + cote_divoire:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ee.png?v8", + couch_and_lamp:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cb.png?v8", + couple:"https://github.githubassets.com/images/icons/emoji/unicode/1f46b.png?v8", + couple_with_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f491.png?v8", + couple_with_heart_man_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2764-1f468.png?v8", + couple_with_heart_woman_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f468.png?v8", + couple_with_heart_woman_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f469.png?v8", + couplekiss:"https://github.githubassets.com/images/icons/emoji/unicode/1f48f.png?v8", + couplekiss_man_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2764-1f48b-1f468.png?v8", + couplekiss_man_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f48b-1f468.png?v8", + couplekiss_woman_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2764-1f48b-1f469.png?v8", + cow:"https://github.githubassets.com/images/icons/emoji/unicode/1f42e.png?v8", + cow2:"https://github.githubassets.com/images/icons/emoji/unicode/1f404.png?v8", + cowboy_hat_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f920.png?v8", + crab:"https://github.githubassets.com/images/icons/emoji/unicode/1f980.png?v8", + crayon:"https://github.githubassets.com/images/icons/emoji/unicode/1f58d.png?v8", + credit_card:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b3.png?v8", + crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f319.png?v8", + cricket:"https://github.githubassets.com/images/icons/emoji/unicode/1f997.png?v8", + cricket_game:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cf.png?v8", + croatia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f7.png?v8", + crocodile:"https://github.githubassets.com/images/icons/emoji/unicode/1f40a.png?v8", + croissant:"https://github.githubassets.com/images/icons/emoji/unicode/1f950.png?v8", + crossed_fingers:"https://github.githubassets.com/images/icons/emoji/unicode/1f91e.png?v8", + crossed_flags:"https://github.githubassets.com/images/icons/emoji/unicode/1f38c.png?v8", + crossed_swords:"https://github.githubassets.com/images/icons/emoji/unicode/2694.png?v8", + crown:"https://github.githubassets.com/images/icons/emoji/unicode/1f451.png?v8", + cry:"https://github.githubassets.com/images/icons/emoji/unicode/1f622.png?v8", + crying_cat_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f63f.png?v8", + crystal_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f52e.png?v8", + cuba:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fa.png?v8", + cucumber:"https://github.githubassets.com/images/icons/emoji/unicode/1f952.png?v8", + cup_with_straw:"https://github.githubassets.com/images/icons/emoji/unicode/1f964.png?v8", + cupcake:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c1.png?v8", + cupid:"https://github.githubassets.com/images/icons/emoji/unicode/1f498.png?v8", + curacao:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fc.png?v8", + curling_stone:"https://github.githubassets.com/images/icons/emoji/unicode/1f94c.png?v8", + curly_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b1.png?v8", + curly_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b1.png?v8", + curly_loop:"https://github.githubassets.com/images/icons/emoji/unicode/27b0.png?v8", + currency_exchange:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b1.png?v8", + curry:"https://github.githubassets.com/images/icons/emoji/unicode/1f35b.png?v8", + cursing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92c.png?v8", + custard:"https://github.githubassets.com/images/icons/emoji/unicode/1f36e.png?v8", + customs:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c3.png?v8", + cut_of_meat:"https://github.githubassets.com/images/icons/emoji/unicode/1f969.png?v8", + cyclone:"https://github.githubassets.com/images/icons/emoji/unicode/1f300.png?v8", + cyprus:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1fe.png?v8", + czech_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ff.png?v8", + dagger:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e1.png?v8", + dancer:"https://github.githubassets.com/images/icons/emoji/unicode/1f483.png?v8", + dancers:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f.png?v8", + dancing_men:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f-2642.png?v8", + dancing_women:"https://github.githubassets.com/images/icons/emoji/unicode/1f46f-2640.png?v8", + dango:"https://github.githubassets.com/images/icons/emoji/unicode/1f361.png?v8", + dark_sunglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f576.png?v8", + dart:"https://github.githubassets.com/images/icons/emoji/unicode/1f3af.png?v8", + dash:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a8.png?v8", + date:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c5.png?v8", + de:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ea.png?v8", + deaf_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf-2642.png?v8", + deaf_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf.png?v8", + deaf_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cf-2640.png?v8", + deciduous_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f333.png?v8", + deer:"https://github.githubassets.com/images/icons/emoji/unicode/1f98c.png?v8", + denmark:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f0.png?v8", + department_store:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ec.png?v8", + derelict_house:"https://github.githubassets.com/images/icons/emoji/unicode/1f3da.png?v8", + desert:"https://github.githubassets.com/images/icons/emoji/unicode/1f3dc.png?v8", + desert_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f3dd.png?v8", + desktop_computer:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a5.png?v8", + detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575.png?v8", + diamond_shape_with_a_dot_inside:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a0.png?v8", + diamonds:"https://github.githubassets.com/images/icons/emoji/unicode/2666.png?v8", + diego_garcia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ec.png?v8", + disappointed:"https://github.githubassets.com/images/icons/emoji/unicode/1f61e.png?v8", + disappointed_relieved:"https://github.githubassets.com/images/icons/emoji/unicode/1f625.png?v8", + diving_mask:"https://github.githubassets.com/images/icons/emoji/unicode/1f93f.png?v8", + diya_lamp:"https://github.githubassets.com/images/icons/emoji/unicode/1fa94.png?v8", + dizzy:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ab.png?v8", + dizzy_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f635.png?v8", + djibouti:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1ef.png?v8", + dna:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ec.png?v8", + do_not_litter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6af.png?v8", + dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f436.png?v8", + dog2:"https://github.githubassets.com/images/icons/emoji/unicode/1f415.png?v8", + dollar:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b5.png?v8", + dolls:"https://github.githubassets.com/images/icons/emoji/unicode/1f38e.png?v8", + dolphin:"https://github.githubassets.com/images/icons/emoji/unicode/1f42c.png?v8", + dominica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f2.png?v8", + dominican_republic:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e9-1f1f4.png?v8", + door:"https://github.githubassets.com/images/icons/emoji/unicode/1f6aa.png?v8", + doughnut:"https://github.githubassets.com/images/icons/emoji/unicode/1f369.png?v8", + dove:"https://github.githubassets.com/images/icons/emoji/unicode/1f54a.png?v8", + dragon:"https://github.githubassets.com/images/icons/emoji/unicode/1f409.png?v8", + dragon_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f432.png?v8", + dress:"https://github.githubassets.com/images/icons/emoji/unicode/1f457.png?v8", + dromedary_camel:"https://github.githubassets.com/images/icons/emoji/unicode/1f42a.png?v8", + drooling_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f924.png?v8", + drop_of_blood:"https://github.githubassets.com/images/icons/emoji/unicode/1fa78.png?v8", + droplet:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a7.png?v8", + drum:"https://github.githubassets.com/images/icons/emoji/unicode/1f941.png?v8", + duck:"https://github.githubassets.com/images/icons/emoji/unicode/1f986.png?v8", + dumpling:"https://github.githubassets.com/images/icons/emoji/unicode/1f95f.png?v8", + dvd:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c0.png?v8", + "e-mail":"https://github.githubassets.com/images/icons/emoji/unicode/1f4e7.png?v8", + eagle:"https://github.githubassets.com/images/icons/emoji/unicode/1f985.png?v8", + ear:"https://github.githubassets.com/images/icons/emoji/unicode/1f442.png?v8", + ear_of_rice:"https://github.githubassets.com/images/icons/emoji/unicode/1f33e.png?v8", + ear_with_hearing_aid:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bb.png?v8", + earth_africa:"https://github.githubassets.com/images/icons/emoji/unicode/1f30d.png?v8", + earth_americas:"https://github.githubassets.com/images/icons/emoji/unicode/1f30e.png?v8", + earth_asia:"https://github.githubassets.com/images/icons/emoji/unicode/1f30f.png?v8", + ecuador:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1e8.png?v8", + egg:"https://github.githubassets.com/images/icons/emoji/unicode/1f95a.png?v8", + eggplant:"https://github.githubassets.com/images/icons/emoji/unicode/1f346.png?v8", + egypt:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ec.png?v8", + eight:"https://github.githubassets.com/images/icons/emoji/unicode/0038-20e3.png?v8", + eight_pointed_black_star:"https://github.githubassets.com/images/icons/emoji/unicode/2734.png?v8", + eight_spoked_asterisk:"https://github.githubassets.com/images/icons/emoji/unicode/2733.png?v8", + eject_button:"https://github.githubassets.com/images/icons/emoji/unicode/23cf.png?v8", + el_salvador:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fb.png?v8", + electric_plug:"https://github.githubassets.com/images/icons/emoji/unicode/1f50c.png?v8", + electron:"https://github.githubassets.com/images/icons/emoji/electron.png?v8", + elephant:"https://github.githubassets.com/images/icons/emoji/unicode/1f418.png?v8", + elf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd.png?v8", + elf_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd-2642.png?v8", + elf_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dd-2640.png?v8", + email:"https://github.githubassets.com/images/icons/emoji/unicode/2709.png?v8", + end:"https://github.githubassets.com/images/icons/emoji/unicode/1f51a.png?v8", + england:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0065-e006e-e0067-e007f.png?v8", + envelope:"https://github.githubassets.com/images/icons/emoji/unicode/2709.png?v8", + envelope_with_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e9.png?v8", + equatorial_guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f6.png?v8", + eritrea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f7.png?v8", + es:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f8.png?v8", + estonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ea.png?v8", + ethiopia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1f9.png?v8", + eu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1fa.png?v8", + euro:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b6.png?v8", + european_castle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f0.png?v8", + european_post_office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e4.png?v8", + european_union:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1fa.png?v8", + evergreen_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f332.png?v8", + exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2757.png?v8", + exploding_head:"https://github.githubassets.com/images/icons/emoji/unicode/1f92f.png?v8", + expressionless:"https://github.githubassets.com/images/icons/emoji/unicode/1f611.png?v8", + eye:"https://github.githubassets.com/images/icons/emoji/unicode/1f441.png?v8", + eye_speech_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f441-1f5e8.png?v8", + eyeglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f453.png?v8", + eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f440.png?v8", + face_with_head_bandage:"https://github.githubassets.com/images/icons/emoji/unicode/1f915.png?v8", + face_with_thermometer:"https://github.githubassets.com/images/icons/emoji/unicode/1f912.png?v8", + facepalm:"https://github.githubassets.com/images/icons/emoji/unicode/1f926.png?v8", + facepunch:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8", + factory:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ed.png?v8", + factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3ed.png?v8", + fairy:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da.png?v8", + fairy_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da-2642.png?v8", + fairy_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9da-2640.png?v8", + falafel:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c6.png?v8", + falkland_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f0.png?v8", + fallen_leaf:"https://github.githubassets.com/images/icons/emoji/unicode/1f342.png?v8", + family:"https://github.githubassets.com/images/icons/emoji/unicode/1f46a.png?v8", + family_man_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f466.png?v8", + family_man_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f466-1f466.png?v8", + family_man_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467.png?v8", + family_man_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467-1f466.png?v8", + family_man_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f467-1f467.png?v8", + family_man_man_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f466.png?v8", + family_man_man_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f466-1f466.png?v8", + family_man_man_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467.png?v8", + family_man_man_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467-1f466.png?v8", + family_man_man_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f468-1f467-1f467.png?v8", + family_man_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f466.png?v8", + family_man_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f466-1f466.png?v8", + family_man_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467.png?v8", + family_man_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467-1f466.png?v8", + family_man_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f469-1f467-1f467.png?v8", + family_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f466.png?v8", + family_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f466-1f466.png?v8", + family_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467.png?v8", + family_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467-1f466.png?v8", + family_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f467-1f467.png?v8", + family_woman_woman_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f466.png?v8", + family_woman_woman_boy_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f466-1f466.png?v8", + family_woman_woman_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467.png?v8", + family_woman_woman_girl_boy:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467-1f466.png?v8", + family_woman_woman_girl_girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f469-1f467-1f467.png?v8", + farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f33e.png?v8", + faroe_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f4.png?v8", + fast_forward:"https://github.githubassets.com/images/icons/emoji/unicode/23e9.png?v8", + fax:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e0.png?v8", + fearful:"https://github.githubassets.com/images/icons/emoji/unicode/1f628.png?v8", + feelsgood:"https://github.githubassets.com/images/icons/emoji/feelsgood.png?v8", + feet:"https://github.githubassets.com/images/icons/emoji/unicode/1f43e.png?v8", + female_detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575-2640.png?v8", + female_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2640.png?v8", + ferris_wheel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a1.png?v8", + ferry:"https://github.githubassets.com/images/icons/emoji/unicode/26f4.png?v8", + field_hockey:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d1.png?v8", + fiji:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1ef.png?v8", + file_cabinet:"https://github.githubassets.com/images/icons/emoji/unicode/1f5c4.png?v8", + file_folder:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c1.png?v8", + film_projector:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fd.png?v8", + film_strip:"https://github.githubassets.com/images/icons/emoji/unicode/1f39e.png?v8", + finland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1ee.png?v8", + finnadie:"https://github.githubassets.com/images/icons/emoji/finnadie.png?v8", + fire:"https://github.githubassets.com/images/icons/emoji/unicode/1f525.png?v8", + fire_engine:"https://github.githubassets.com/images/icons/emoji/unicode/1f692.png?v8", + fire_extinguisher:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ef.png?v8", + firecracker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e8.png?v8", + firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f692.png?v8", + fireworks:"https://github.githubassets.com/images/icons/emoji/unicode/1f386.png?v8", + first_quarter_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f313.png?v8", + first_quarter_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31b.png?v8", + fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f41f.png?v8", + fish_cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f365.png?v8", + fishing_pole_and_fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a3.png?v8", + fist:"https://github.githubassets.com/images/icons/emoji/unicode/270a.png?v8", + fist_left:"https://github.githubassets.com/images/icons/emoji/unicode/1f91b.png?v8", + fist_oncoming:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8", + fist_raised:"https://github.githubassets.com/images/icons/emoji/unicode/270a.png?v8", + fist_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f91c.png?v8", + five:"https://github.githubassets.com/images/icons/emoji/unicode/0035-20e3.png?v8", + flags:"https://github.githubassets.com/images/icons/emoji/unicode/1f38f.png?v8", + flamingo:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a9.png?v8", + flashlight:"https://github.githubassets.com/images/icons/emoji/unicode/1f526.png?v8", + flat_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f97f.png?v8", + fleur_de_lis:"https://github.githubassets.com/images/icons/emoji/unicode/269c.png?v8", + flight_arrival:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ec.png?v8", + flight_departure:"https://github.githubassets.com/images/icons/emoji/unicode/1f6eb.png?v8", + flipper:"https://github.githubassets.com/images/icons/emoji/unicode/1f42c.png?v8", + floppy_disk:"https://github.githubassets.com/images/icons/emoji/unicode/1f4be.png?v8", + flower_playing_cards:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b4.png?v8", + flushed:"https://github.githubassets.com/images/icons/emoji/unicode/1f633.png?v8", + flying_disc:"https://github.githubassets.com/images/icons/emoji/unicode/1f94f.png?v8", + flying_saucer:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f8.png?v8", + fog:"https://github.githubassets.com/images/icons/emoji/unicode/1f32b.png?v8", + foggy:"https://github.githubassets.com/images/icons/emoji/unicode/1f301.png?v8", + foot:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b6.png?v8", + football:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c8.png?v8", + footprints:"https://github.githubassets.com/images/icons/emoji/unicode/1f463.png?v8", + fork_and_knife:"https://github.githubassets.com/images/icons/emoji/unicode/1f374.png?v8", + fortune_cookie:"https://github.githubassets.com/images/icons/emoji/unicode/1f960.png?v8", + fountain:"https://github.githubassets.com/images/icons/emoji/unicode/26f2.png?v8", + fountain_pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f58b.png?v8", + four:"https://github.githubassets.com/images/icons/emoji/unicode/0034-20e3.png?v8", + four_leaf_clover:"https://github.githubassets.com/images/icons/emoji/unicode/1f340.png?v8", + fox_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f98a.png?v8", + fr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f7.png?v8", + framed_picture:"https://github.githubassets.com/images/icons/emoji/unicode/1f5bc.png?v8", + free:"https://github.githubassets.com/images/icons/emoji/unicode/1f193.png?v8", + french_guiana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1eb.png?v8", + french_polynesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1eb.png?v8", + french_southern_territories:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1eb.png?v8", + fried_egg:"https://github.githubassets.com/images/icons/emoji/unicode/1f373.png?v8", + fried_shrimp:"https://github.githubassets.com/images/icons/emoji/unicode/1f364.png?v8", + fries:"https://github.githubassets.com/images/icons/emoji/unicode/1f35f.png?v8", + frog:"https://github.githubassets.com/images/icons/emoji/unicode/1f438.png?v8", + frowning:"https://github.githubassets.com/images/icons/emoji/unicode/1f626.png?v8", + frowning_face:"https://github.githubassets.com/images/icons/emoji/unicode/2639.png?v8", + frowning_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d-2642.png?v8", + frowning_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d.png?v8", + frowning_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64d-2640.png?v8", + fu:"https://github.githubassets.com/images/icons/emoji/unicode/1f595.png?v8", + fuelpump:"https://github.githubassets.com/images/icons/emoji/unicode/26fd.png?v8", + full_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f315.png?v8", + full_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31d.png?v8", + funeral_urn:"https://github.githubassets.com/images/icons/emoji/unicode/26b1.png?v8", + gabon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e6.png?v8", + gambia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f2.png?v8", + game_die:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b2.png?v8", + garlic:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c4.png?v8", + gb:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e7.png?v8", + gear:"https://github.githubassets.com/images/icons/emoji/unicode/2699.png?v8", + gem:"https://github.githubassets.com/images/icons/emoji/unicode/1f48e.png?v8", + gemini:"https://github.githubassets.com/images/icons/emoji/unicode/264a.png?v8", + genie:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de.png?v8", + genie_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de-2642.png?v8", + genie_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9de-2640.png?v8", + georgia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ea.png?v8", + ghana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ed.png?v8", + ghost:"https://github.githubassets.com/images/icons/emoji/unicode/1f47b.png?v8", + gibraltar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ee.png?v8", + gift:"https://github.githubassets.com/images/icons/emoji/unicode/1f381.png?v8", + gift_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49d.png?v8", + giraffe:"https://github.githubassets.com/images/icons/emoji/unicode/1f992.png?v8", + girl:"https://github.githubassets.com/images/icons/emoji/unicode/1f467.png?v8", + globe_with_meridians:"https://github.githubassets.com/images/icons/emoji/unicode/1f310.png?v8", + gloves:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e4.png?v8", + goal_net:"https://github.githubassets.com/images/icons/emoji/unicode/1f945.png?v8", + goat:"https://github.githubassets.com/images/icons/emoji/unicode/1f410.png?v8", + goberserk:"https://github.githubassets.com/images/icons/emoji/goberserk.png?v8", + godmode:"https://github.githubassets.com/images/icons/emoji/godmode.png?v8", + goggles:"https://github.githubassets.com/images/icons/emoji/unicode/1f97d.png?v8", + golf:"https://github.githubassets.com/images/icons/emoji/unicode/26f3.png?v8", + golfing:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc.png?v8", + golfing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc-2642.png?v8", + golfing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cc-2640.png?v8", + gorilla:"https://github.githubassets.com/images/icons/emoji/unicode/1f98d.png?v8", + grapes:"https://github.githubassets.com/images/icons/emoji/unicode/1f347.png?v8", + greece:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f7.png?v8", + green_apple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34f.png?v8", + green_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d7.png?v8", + green_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e2.png?v8", + green_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49a.png?v8", + green_salad:"https://github.githubassets.com/images/icons/emoji/unicode/1f957.png?v8", + green_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e9.png?v8", + greenland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f1.png?v8", + grenada:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e9.png?v8", + grey_exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2755.png?v8", + grey_question:"https://github.githubassets.com/images/icons/emoji/unicode/2754.png?v8", + grimacing:"https://github.githubassets.com/images/icons/emoji/unicode/1f62c.png?v8", + grin:"https://github.githubassets.com/images/icons/emoji/unicode/1f601.png?v8", + grinning:"https://github.githubassets.com/images/icons/emoji/unicode/1f600.png?v8", + guadeloupe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f5.png?v8", + guam:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fa.png?v8", + guard:"https://github.githubassets.com/images/icons/emoji/unicode/1f482.png?v8", + guardsman:"https://github.githubassets.com/images/icons/emoji/unicode/1f482-2642.png?v8", + guardswoman:"https://github.githubassets.com/images/icons/emoji/unicode/1f482-2640.png?v8", + guatemala:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f9.png?v8", + guernsey:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1ec.png?v8", + guide_dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ae.png?v8", + guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f3.png?v8", + guinea_bissau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fc.png?v8", + guitar:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b8.png?v8", + gun:"https://github.githubassets.com/images/icons/emoji/unicode/1f52b.png?v8", + guyana:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1fe.png?v8", + haircut:"https://github.githubassets.com/images/icons/emoji/unicode/1f487.png?v8", + haircut_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f487-2642.png?v8", + haircut_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f487-2640.png?v8", + haiti:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f9.png?v8", + hamburger:"https://github.githubassets.com/images/icons/emoji/unicode/1f354.png?v8", + hammer:"https://github.githubassets.com/images/icons/emoji/unicode/1f528.png?v8", + hammer_and_pick:"https://github.githubassets.com/images/icons/emoji/unicode/2692.png?v8", + hammer_and_wrench:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e0.png?v8", + hamster:"https://github.githubassets.com/images/icons/emoji/unicode/1f439.png?v8", + hand:"https://github.githubassets.com/images/icons/emoji/unicode/270b.png?v8", + hand_over_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f92d.png?v8", + handbag:"https://github.githubassets.com/images/icons/emoji/unicode/1f45c.png?v8", + handball_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e.png?v8", + handshake:"https://github.githubassets.com/images/icons/emoji/unicode/1f91d.png?v8", + hankey:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8", + hash:"https://github.githubassets.com/images/icons/emoji/unicode/0023-20e3.png?v8", + hatched_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f425.png?v8", + hatching_chick:"https://github.githubassets.com/images/icons/emoji/unicode/1f423.png?v8", + headphones:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a7.png?v8", + health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2695.png?v8", + hear_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f649.png?v8", + heard_mcdonald_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f2.png?v8", + heart:"https://github.githubassets.com/images/icons/emoji/unicode/2764.png?v8", + heart_decoration:"https://github.githubassets.com/images/icons/emoji/unicode/1f49f.png?v8", + heart_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f60d.png?v8", + heart_eyes_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63b.png?v8", + heartbeat:"https://github.githubassets.com/images/icons/emoji/unicode/1f493.png?v8", + heartpulse:"https://github.githubassets.com/images/icons/emoji/unicode/1f497.png?v8", + hearts:"https://github.githubassets.com/images/icons/emoji/unicode/2665.png?v8", + heavy_check_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2714.png?v8", + heavy_division_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2797.png?v8", + heavy_dollar_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b2.png?v8", + heavy_exclamation_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2757.png?v8", + heavy_heart_exclamation:"https://github.githubassets.com/images/icons/emoji/unicode/2763.png?v8", + heavy_minus_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2796.png?v8", + heavy_multiplication_x:"https://github.githubassets.com/images/icons/emoji/unicode/2716.png?v8", + heavy_plus_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2795.png?v8", + hedgehog:"https://github.githubassets.com/images/icons/emoji/unicode/1f994.png?v8", + helicopter:"https://github.githubassets.com/images/icons/emoji/unicode/1f681.png?v8", + herb:"https://github.githubassets.com/images/icons/emoji/unicode/1f33f.png?v8", + hibiscus:"https://github.githubassets.com/images/icons/emoji/unicode/1f33a.png?v8", + high_brightness:"https://github.githubassets.com/images/icons/emoji/unicode/1f506.png?v8", + high_heel:"https://github.githubassets.com/images/icons/emoji/unicode/1f460.png?v8", + hiking_boot:"https://github.githubassets.com/images/icons/emoji/unicode/1f97e.png?v8", + hindu_temple:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d5.png?v8", + hippopotamus:"https://github.githubassets.com/images/icons/emoji/unicode/1f99b.png?v8", + hocho:"https://github.githubassets.com/images/icons/emoji/unicode/1f52a.png?v8", + hole:"https://github.githubassets.com/images/icons/emoji/unicode/1f573.png?v8", + honduras:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f3.png?v8", + honey_pot:"https://github.githubassets.com/images/icons/emoji/unicode/1f36f.png?v8", + honeybee:"https://github.githubassets.com/images/icons/emoji/unicode/1f41d.png?v8", + hong_kong:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1f0.png?v8", + horse:"https://github.githubassets.com/images/icons/emoji/unicode/1f434.png?v8", + horse_racing:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c7.png?v8", + hospital:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e5.png?v8", + hot_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f975.png?v8", + hot_pepper:"https://github.githubassets.com/images/icons/emoji/unicode/1f336.png?v8", + hotdog:"https://github.githubassets.com/images/icons/emoji/unicode/1f32d.png?v8", + hotel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e8.png?v8", + hotsprings:"https://github.githubassets.com/images/icons/emoji/unicode/2668.png?v8", + hourglass:"https://github.githubassets.com/images/icons/emoji/unicode/231b.png?v8", + hourglass_flowing_sand:"https://github.githubassets.com/images/icons/emoji/unicode/23f3.png?v8", + house:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e0.png?v8", + house_with_garden:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e1.png?v8", + houses:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d8.png?v8", + hugs:"https://github.githubassets.com/images/icons/emoji/unicode/1f917.png?v8", + hungary:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ed-1f1fa.png?v8", + hurtrealbad:"https://github.githubassets.com/images/icons/emoji/hurtrealbad.png?v8", + hushed:"https://github.githubassets.com/images/icons/emoji/unicode/1f62f.png?v8", + ice_cream:"https://github.githubassets.com/images/icons/emoji/unicode/1f368.png?v8", + ice_cube:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ca.png?v8", + ice_hockey:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d2.png?v8", + ice_skate:"https://github.githubassets.com/images/icons/emoji/unicode/26f8.png?v8", + icecream:"https://github.githubassets.com/images/icons/emoji/unicode/1f366.png?v8", + iceland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f8.png?v8", + id:"https://github.githubassets.com/images/icons/emoji/unicode/1f194.png?v8", + ideograph_advantage:"https://github.githubassets.com/images/icons/emoji/unicode/1f250.png?v8", + imp:"https://github.githubassets.com/images/icons/emoji/unicode/1f47f.png?v8", + inbox_tray:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e5.png?v8", + incoming_envelope:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e8.png?v8", + india:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f3.png?v8", + indonesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1e9.png?v8", + infinity:"https://github.githubassets.com/images/icons/emoji/unicode/267e.png?v8", + information_desk_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f481.png?v8", + information_source:"https://github.githubassets.com/images/icons/emoji/unicode/2139.png?v8", + innocent:"https://github.githubassets.com/images/icons/emoji/unicode/1f607.png?v8", + interrobang:"https://github.githubassets.com/images/icons/emoji/unicode/2049.png?v8", + iphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f1.png?v8", + iran:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f7.png?v8", + iraq:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f6.png?v8", + ireland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1ea.png?v8", + isle_of_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f2.png?v8", + israel:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f1.png?v8", + it:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ee-1f1f9.png?v8", + izakaya_lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ee.png?v8", + jack_o_lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f383.png?v8", + jamaica:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f2.png?v8", + japan:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fe.png?v8", + japanese_castle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ef.png?v8", + japanese_goblin:"https://github.githubassets.com/images/icons/emoji/unicode/1f47a.png?v8", + japanese_ogre:"https://github.githubassets.com/images/icons/emoji/unicode/1f479.png?v8", + jeans:"https://github.githubassets.com/images/icons/emoji/unicode/1f456.png?v8", + jersey:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1ea.png?v8", + jigsaw:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e9.png?v8", + jordan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f4.png?v8", + joy:"https://github.githubassets.com/images/icons/emoji/unicode/1f602.png?v8", + joy_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f639.png?v8", + joystick:"https://github.githubassets.com/images/icons/emoji/unicode/1f579.png?v8", + jp:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ef-1f1f5.png?v8", + judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2696.png?v8", + juggling_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f939.png?v8", + kaaba:"https://github.githubassets.com/images/icons/emoji/unicode/1f54b.png?v8", + kangaroo:"https://github.githubassets.com/images/icons/emoji/unicode/1f998.png?v8", + kazakhstan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ff.png?v8", + kenya:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ea.png?v8", + key:"https://github.githubassets.com/images/icons/emoji/unicode/1f511.png?v8", + keyboard:"https://github.githubassets.com/images/icons/emoji/unicode/2328.png?v8", + keycap_ten:"https://github.githubassets.com/images/icons/emoji/unicode/1f51f.png?v8", + kick_scooter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f4.png?v8", + kimono:"https://github.githubassets.com/images/icons/emoji/unicode/1f458.png?v8", + kiribati:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ee.png?v8", + kiss:"https://github.githubassets.com/images/icons/emoji/unicode/1f48b.png?v8", + kissing:"https://github.githubassets.com/images/icons/emoji/unicode/1f617.png?v8", + kissing_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63d.png?v8", + kissing_closed_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f61a.png?v8", + kissing_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f618.png?v8", + kissing_smiling_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f619.png?v8", + kite:"https://github.githubassets.com/images/icons/emoji/unicode/1fa81.png?v8", + kiwi_fruit:"https://github.githubassets.com/images/icons/emoji/unicode/1f95d.png?v8", + kneeling_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce-2642.png?v8", + kneeling_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce.png?v8", + kneeling_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ce-2640.png?v8", + knife:"https://github.githubassets.com/images/icons/emoji/unicode/1f52a.png?v8", + koala:"https://github.githubassets.com/images/icons/emoji/unicode/1f428.png?v8", + koko:"https://github.githubassets.com/images/icons/emoji/unicode/1f201.png?v8", + kosovo:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fd-1f1f0.png?v8", + kr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f7.png?v8", + kuwait:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1fc.png?v8", + kyrgyzstan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1ec.png?v8", + lab_coat:"https://github.githubassets.com/images/icons/emoji/unicode/1f97c.png?v8", + label:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f7.png?v8", + lacrosse:"https://github.githubassets.com/images/icons/emoji/unicode/1f94d.png?v8", + lantern:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ee.png?v8", + laos:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e6.png?v8", + large_blue_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f535.png?v8", + large_blue_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f537.png?v8", + large_orange_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f536.png?v8", + last_quarter_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f317.png?v8", + last_quarter_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31c.png?v8", + latin_cross:"https://github.githubassets.com/images/icons/emoji/unicode/271d.png?v8", + latvia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fb.png?v8", + laughing:"https://github.githubassets.com/images/icons/emoji/unicode/1f606.png?v8", + leafy_green:"https://github.githubassets.com/images/icons/emoji/unicode/1f96c.png?v8", + leaves:"https://github.githubassets.com/images/icons/emoji/unicode/1f343.png?v8", + lebanon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e7.png?v8", + ledger:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d2.png?v8", + left_luggage:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c5.png?v8", + left_right_arrow:"https://github.githubassets.com/images/icons/emoji/unicode/2194.png?v8", + left_speech_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e8.png?v8", + leftwards_arrow_with_hook:"https://github.githubassets.com/images/icons/emoji/unicode/21a9.png?v8", + leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b5.png?v8", + lemon:"https://github.githubassets.com/images/icons/emoji/unicode/1f34b.png?v8", + leo:"https://github.githubassets.com/images/icons/emoji/unicode/264c.png?v8", + leopard:"https://github.githubassets.com/images/icons/emoji/unicode/1f406.png?v8", + lesotho:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f8.png?v8", + level_slider:"https://github.githubassets.com/images/icons/emoji/unicode/1f39a.png?v8", + liberia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f7.png?v8", + libra:"https://github.githubassets.com/images/icons/emoji/unicode/264e.png?v8", + libya:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fe.png?v8", + liechtenstein:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1ee.png?v8", + light_rail:"https://github.githubassets.com/images/icons/emoji/unicode/1f688.png?v8", + link:"https://github.githubassets.com/images/icons/emoji/unicode/1f517.png?v8", + lion:"https://github.githubassets.com/images/icons/emoji/unicode/1f981.png?v8", + lips:"https://github.githubassets.com/images/icons/emoji/unicode/1f444.png?v8", + lipstick:"https://github.githubassets.com/images/icons/emoji/unicode/1f484.png?v8", + lithuania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f9.png?v8", + lizard:"https://github.githubassets.com/images/icons/emoji/unicode/1f98e.png?v8", + llama:"https://github.githubassets.com/images/icons/emoji/unicode/1f999.png?v8", + lobster:"https://github.githubassets.com/images/icons/emoji/unicode/1f99e.png?v8", + lock:"https://github.githubassets.com/images/icons/emoji/unicode/1f512.png?v8", + lock_with_ink_pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f50f.png?v8", + lollipop:"https://github.githubassets.com/images/icons/emoji/unicode/1f36d.png?v8", + loop:"https://github.githubassets.com/images/icons/emoji/unicode/27bf.png?v8", + lotion_bottle:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f4.png?v8", + lotus_position:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8.png?v8", + lotus_position_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8-2642.png?v8", + lotus_position_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d8-2640.png?v8", + loud_sound:"https://github.githubassets.com/images/icons/emoji/unicode/1f50a.png?v8", + loudspeaker:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e2.png?v8", + love_hotel:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e9.png?v8", + love_letter:"https://github.githubassets.com/images/icons/emoji/unicode/1f48c.png?v8", + love_you_gesture:"https://github.githubassets.com/images/icons/emoji/unicode/1f91f.png?v8", + low_brightness:"https://github.githubassets.com/images/icons/emoji/unicode/1f505.png?v8", + luggage:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f3.png?v8", + luxembourg:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1fa.png?v8", + lying_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f925.png?v8", + m:"https://github.githubassets.com/images/icons/emoji/unicode/24c2.png?v8", + macau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f4.png?v8", + macedonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f0.png?v8", + madagascar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ec.png?v8", + mag:"https://github.githubassets.com/images/icons/emoji/unicode/1f50d.png?v8", + mag_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f50e.png?v8", + mage:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9.png?v8", + mage_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9-2642.png?v8", + mage_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d9-2640.png?v8", + magnet:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f2.png?v8", + mahjong:"https://github.githubassets.com/images/icons/emoji/unicode/1f004.png?v8", + mailbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f4eb.png?v8", + mailbox_closed:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ea.png?v8", + mailbox_with_mail:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ec.png?v8", + mailbox_with_no_mail:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ed.png?v8", + malawi:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fc.png?v8", + malaysia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fe.png?v8", + maldives:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fb.png?v8", + male_detective:"https://github.githubassets.com/images/icons/emoji/unicode/1f575-2642.png?v8", + male_sign:"https://github.githubassets.com/images/icons/emoji/unicode/2642.png?v8", + mali:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f1.png?v8", + malta:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f9.png?v8", + man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468.png?v8", + man_artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3a8.png?v8", + man_astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f680.png?v8", + man_cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938-2642.png?v8", + man_cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f373.png?v8", + man_dancing:"https://github.githubassets.com/images/icons/emoji/unicode/1f57a.png?v8", + man_facepalming:"https://github.githubassets.com/images/icons/emoji/unicode/1f926-2642.png?v8", + man_factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3ed.png?v8", + man_farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f33e.png?v8", + man_firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f692.png?v8", + man_health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2695.png?v8", + man_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9bd.png?v8", + man_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9bc.png?v8", + man_in_tuxedo:"https://github.githubassets.com/images/icons/emoji/unicode/1f935.png?v8", + man_judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2696.png?v8", + man_juggling:"https://github.githubassets.com/images/icons/emoji/unicode/1f939-2642.png?v8", + man_mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f527.png?v8", + man_office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f4bc.png?v8", + man_pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-2708.png?v8", + man_playing_handball:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e-2642.png?v8", + man_playing_water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d-2642.png?v8", + man_scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f52c.png?v8", + man_shrugging:"https://github.githubassets.com/images/icons/emoji/unicode/1f937-2642.png?v8", + man_singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3a4.png?v8", + man_student:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f393.png?v8", + man_teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f3eb.png?v8", + man_technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f4bb.png?v8", + man_with_gua_pi_mao:"https://github.githubassets.com/images/icons/emoji/unicode/1f472.png?v8", + man_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9af.png?v8", + man_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473-2642.png?v8", + mandarin:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8", + mango:"https://github.githubassets.com/images/icons/emoji/unicode/1f96d.png?v8", + mans_shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45e.png?v8", + mantelpiece_clock:"https://github.githubassets.com/images/icons/emoji/unicode/1f570.png?v8", + manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bd.png?v8", + maple_leaf:"https://github.githubassets.com/images/icons/emoji/unicode/1f341.png?v8", + marshall_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ed.png?v8", + martial_arts_uniform:"https://github.githubassets.com/images/icons/emoji/unicode/1f94b.png?v8", + martinique:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f6.png?v8", + mask:"https://github.githubassets.com/images/icons/emoji/unicode/1f637.png?v8", + massage:"https://github.githubassets.com/images/icons/emoji/unicode/1f486.png?v8", + massage_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f486-2642.png?v8", + massage_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f486-2640.png?v8", + mate:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c9.png?v8", + mauritania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f7.png?v8", + mauritius:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fa.png?v8", + mayotte:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fe-1f1f9.png?v8", + meat_on_bone:"https://github.githubassets.com/images/icons/emoji/unicode/1f356.png?v8", + mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f527.png?v8", + mechanical_arm:"https://github.githubassets.com/images/icons/emoji/unicode/1f9be.png?v8", + mechanical_leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bf.png?v8", + medal_military:"https://github.githubassets.com/images/icons/emoji/unicode/1f396.png?v8", + medal_sports:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c5.png?v8", + medical_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/2695.png?v8", + mega:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e3.png?v8", + melon:"https://github.githubassets.com/images/icons/emoji/unicode/1f348.png?v8", + memo:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dd.png?v8", + men_wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c-2642.png?v8", + menorah:"https://github.githubassets.com/images/icons/emoji/unicode/1f54e.png?v8", + mens:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b9.png?v8", + mermaid:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc-2640.png?v8", + merman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc-2642.png?v8", + merperson:"https://github.githubassets.com/images/icons/emoji/unicode/1f9dc.png?v8", + metal:"https://github.githubassets.com/images/icons/emoji/unicode/1f918.png?v8", + metro:"https://github.githubassets.com/images/icons/emoji/unicode/1f687.png?v8", + mexico:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1fd.png?v8", + microbe:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a0.png?v8", + micronesia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1eb-1f1f2.png?v8", + microphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a4.png?v8", + microscope:"https://github.githubassets.com/images/icons/emoji/unicode/1f52c.png?v8", + middle_finger:"https://github.githubassets.com/images/icons/emoji/unicode/1f595.png?v8", + milk_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f95b.png?v8", + milky_way:"https://github.githubassets.com/images/icons/emoji/unicode/1f30c.png?v8", + minibus:"https://github.githubassets.com/images/icons/emoji/unicode/1f690.png?v8", + minidisc:"https://github.githubassets.com/images/icons/emoji/unicode/1f4bd.png?v8", + mobile_phone_off:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f4.png?v8", + moldova:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e9.png?v8", + monaco:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e8.png?v8", + money_mouth_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f911.png?v8", + money_with_wings:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b8.png?v8", + moneybag:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b0.png?v8", + mongolia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f3.png?v8", + monkey:"https://github.githubassets.com/images/icons/emoji/unicode/1f412.png?v8", + monkey_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f435.png?v8", + monocle_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d0.png?v8", + monorail:"https://github.githubassets.com/images/icons/emoji/unicode/1f69d.png?v8", + montenegro:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ea.png?v8", + montserrat:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f8.png?v8", + moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f314.png?v8", + moon_cake:"https://github.githubassets.com/images/icons/emoji/unicode/1f96e.png?v8", + morocco:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1e6.png?v8", + mortar_board:"https://github.githubassets.com/images/icons/emoji/unicode/1f393.png?v8", + mosque:"https://github.githubassets.com/images/icons/emoji/unicode/1f54c.png?v8", + mosquito:"https://github.githubassets.com/images/icons/emoji/unicode/1f99f.png?v8", + motor_boat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e5.png?v8", + motor_scooter:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f5.png?v8", + motorcycle:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cd.png?v8", + motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9bc.png?v8", + motorway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e3.png?v8", + mount_fuji:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fb.png?v8", + mountain:"https://github.githubassets.com/images/icons/emoji/unicode/26f0.png?v8", + mountain_bicyclist:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5.png?v8", + mountain_biking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5-2642.png?v8", + mountain_biking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b5-2640.png?v8", + mountain_cableway:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a0.png?v8", + mountain_railway:"https://github.githubassets.com/images/icons/emoji/unicode/1f69e.png?v8", + mountain_snow:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d4.png?v8", + mouse:"https://github.githubassets.com/images/icons/emoji/unicode/1f42d.png?v8", + mouse2:"https://github.githubassets.com/images/icons/emoji/unicode/1f401.png?v8", + movie_camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a5.png?v8", + moyai:"https://github.githubassets.com/images/icons/emoji/unicode/1f5ff.png?v8", + mozambique:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1ff.png?v8", + mrs_claus:"https://github.githubassets.com/images/icons/emoji/unicode/1f936.png?v8", + muscle:"https://github.githubassets.com/images/icons/emoji/unicode/1f4aa.png?v8", + mushroom:"https://github.githubassets.com/images/icons/emoji/unicode/1f344.png?v8", + musical_keyboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b9.png?v8", + musical_note:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b5.png?v8", + musical_score:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bc.png?v8", + mute:"https://github.githubassets.com/images/icons/emoji/unicode/1f507.png?v8", + myanmar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f2.png?v8", + nail_care:"https://github.githubassets.com/images/icons/emoji/unicode/1f485.png?v8", + name_badge:"https://github.githubassets.com/images/icons/emoji/unicode/1f4db.png?v8", + namibia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1e6.png?v8", + national_park:"https://github.githubassets.com/images/icons/emoji/unicode/1f3de.png?v8", + nauru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f7.png?v8", + nauseated_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f922.png?v8", + nazar_amulet:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ff.png?v8", + neckbeard:"https://github.githubassets.com/images/icons/emoji/neckbeard.png?v8", + necktie:"https://github.githubassets.com/images/icons/emoji/unicode/1f454.png?v8", + negative_squared_cross_mark:"https://github.githubassets.com/images/icons/emoji/unicode/274e.png?v8", + nepal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f5.png?v8", + nerd_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f913.png?v8", + netherlands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f1.png?v8", + neutral_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f610.png?v8", + new:"https://github.githubassets.com/images/icons/emoji/unicode/1f195.png?v8", + new_caledonia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1e8.png?v8", + new_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f311.png?v8", + new_moon_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31a.png?v8", + new_zealand:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ff.png?v8", + newspaper:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f0.png?v8", + newspaper_roll:"https://github.githubassets.com/images/icons/emoji/unicode/1f5de.png?v8", + next_track_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ed.png?v8", + ng:"https://github.githubassets.com/images/icons/emoji/unicode/1f196.png?v8", + ng_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2642.png?v8", + ng_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2640.png?v8", + nicaragua:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ee.png?v8", + niger:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ea.png?v8", + nigeria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1ec.png?v8", + night_with_stars:"https://github.githubassets.com/images/icons/emoji/unicode/1f303.png?v8", + nine:"https://github.githubassets.com/images/icons/emoji/unicode/0039-20e3.png?v8", + niue:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1fa.png?v8", + no_bell:"https://github.githubassets.com/images/icons/emoji/unicode/1f515.png?v8", + no_bicycles:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b3.png?v8", + no_entry:"https://github.githubassets.com/images/icons/emoji/unicode/26d4.png?v8", + no_entry_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ab.png?v8", + no_good:"https://github.githubassets.com/images/icons/emoji/unicode/1f645.png?v8", + no_good_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2642.png?v8", + no_good_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f645-2640.png?v8", + no_mobile_phones:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f5.png?v8", + no_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f636.png?v8", + no_pedestrians:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b7.png?v8", + no_smoking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ad.png?v8", + "non-potable_water":"https://github.githubassets.com/images/icons/emoji/unicode/1f6b1.png?v8", + norfolk_island:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1eb.png?v8", + north_korea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f5.png?v8", + northern_mariana_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1f5.png?v8", + norway:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f3-1f1f4.png?v8", + nose:"https://github.githubassets.com/images/icons/emoji/unicode/1f443.png?v8", + notebook:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d3.png?v8", + notebook_with_decorative_cover:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d4.png?v8", + notes:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b6.png?v8", + nut_and_bolt:"https://github.githubassets.com/images/icons/emoji/unicode/1f529.png?v8", + o:"https://github.githubassets.com/images/icons/emoji/unicode/2b55.png?v8", + o2:"https://github.githubassets.com/images/icons/emoji/unicode/1f17e.png?v8", + ocean:"https://github.githubassets.com/images/icons/emoji/unicode/1f30a.png?v8", + octocat:"https://github.githubassets.com/images/icons/emoji/octocat.png?v8", + octopus:"https://github.githubassets.com/images/icons/emoji/unicode/1f419.png?v8", + oden:"https://github.githubassets.com/images/icons/emoji/unicode/1f362.png?v8", + office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e2.png?v8", + office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f4bc.png?v8", + oil_drum:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e2.png?v8", + ok:"https://github.githubassets.com/images/icons/emoji/unicode/1f197.png?v8", + ok_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f44c.png?v8", + ok_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f646-2642.png?v8", + ok_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f646.png?v8", + ok_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f646-2640.png?v8", + old_key:"https://github.githubassets.com/images/icons/emoji/unicode/1f5dd.png?v8", + older_adult:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d3.png?v8", + older_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f474.png?v8", + older_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f475.png?v8", + om:"https://github.githubassets.com/images/icons/emoji/unicode/1f549.png?v8", + oman:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f4-1f1f2.png?v8", + on:"https://github.githubassets.com/images/icons/emoji/unicode/1f51b.png?v8", + oncoming_automobile:"https://github.githubassets.com/images/icons/emoji/unicode/1f698.png?v8", + oncoming_bus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68d.png?v8", + oncoming_police_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f694.png?v8", + oncoming_taxi:"https://github.githubassets.com/images/icons/emoji/unicode/1f696.png?v8", + one:"https://github.githubassets.com/images/icons/emoji/unicode/0031-20e3.png?v8", + one_piece_swimsuit:"https://github.githubassets.com/images/icons/emoji/unicode/1fa71.png?v8", + onion:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c5.png?v8", + open_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d6.png?v8", + open_file_folder:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c2.png?v8", + open_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f450.png?v8", + open_mouth:"https://github.githubassets.com/images/icons/emoji/unicode/1f62e.png?v8", + open_umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/2602.png?v8", + ophiuchus:"https://github.githubassets.com/images/icons/emoji/unicode/26ce.png?v8", + orange:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8", + orange_book:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d9.png?v8", + orange_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e0.png?v8", + orange_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e1.png?v8", + orange_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e7.png?v8", + orangutan:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a7.png?v8", + orthodox_cross:"https://github.githubassets.com/images/icons/emoji/unicode/2626.png?v8", + otter:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a6.png?v8", + outbox_tray:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e4.png?v8", + owl:"https://github.githubassets.com/images/icons/emoji/unicode/1f989.png?v8", + ox:"https://github.githubassets.com/images/icons/emoji/unicode/1f402.png?v8", + oyster:"https://github.githubassets.com/images/icons/emoji/unicode/1f9aa.png?v8", + package:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e6.png?v8", + page_facing_up:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c4.png?v8", + page_with_curl:"https://github.githubassets.com/images/icons/emoji/unicode/1f4c3.png?v8", + pager:"https://github.githubassets.com/images/icons/emoji/unicode/1f4df.png?v8", + paintbrush:"https://github.githubassets.com/images/icons/emoji/unicode/1f58c.png?v8", + pakistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f0.png?v8", + palau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1fc.png?v8", + palestinian_territories:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f8.png?v8", + palm_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f334.png?v8", + palms_up_together:"https://github.githubassets.com/images/icons/emoji/unicode/1f932.png?v8", + panama:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1e6.png?v8", + pancakes:"https://github.githubassets.com/images/icons/emoji/unicode/1f95e.png?v8", + panda_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f43c.png?v8", + paperclip:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ce.png?v8", + paperclips:"https://github.githubassets.com/images/icons/emoji/unicode/1f587.png?v8", + papua_new_guinea:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ec.png?v8", + parachute:"https://github.githubassets.com/images/icons/emoji/unicode/1fa82.png?v8", + paraguay:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1fe.png?v8", + parasol_on_ground:"https://github.githubassets.com/images/icons/emoji/unicode/26f1.png?v8", + parking:"https://github.githubassets.com/images/icons/emoji/unicode/1f17f.png?v8", + parrot:"https://github.githubassets.com/images/icons/emoji/unicode/1f99c.png?v8", + part_alternation_mark:"https://github.githubassets.com/images/icons/emoji/unicode/303d.png?v8", + partly_sunny:"https://github.githubassets.com/images/icons/emoji/unicode/26c5.png?v8", + partying_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f973.png?v8", + passenger_ship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f3.png?v8", + passport_control:"https://github.githubassets.com/images/icons/emoji/unicode/1f6c2.png?v8", + pause_button:"https://github.githubassets.com/images/icons/emoji/unicode/23f8.png?v8", + paw_prints:"https://github.githubassets.com/images/icons/emoji/unicode/1f43e.png?v8", + peace_symbol:"https://github.githubassets.com/images/icons/emoji/unicode/262e.png?v8", + peach:"https://github.githubassets.com/images/icons/emoji/unicode/1f351.png?v8", + peacock:"https://github.githubassets.com/images/icons/emoji/unicode/1f99a.png?v8", + peanuts:"https://github.githubassets.com/images/icons/emoji/unicode/1f95c.png?v8", + pear:"https://github.githubassets.com/images/icons/emoji/unicode/1f350.png?v8", + pen:"https://github.githubassets.com/images/icons/emoji/unicode/1f58a.png?v8", + pencil:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dd.png?v8", + pencil2:"https://github.githubassets.com/images/icons/emoji/unicode/270f.png?v8", + penguin:"https://github.githubassets.com/images/icons/emoji/unicode/1f427.png?v8", + pensive:"https://github.githubassets.com/images/icons/emoji/unicode/1f614.png?v8", + people_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f91d-1f9d1.png?v8", + performing_arts:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ad.png?v8", + persevere:"https://github.githubassets.com/images/icons/emoji/unicode/1f623.png?v8", + person_bald:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b2.png?v8", + person_curly_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b1.png?v8", + person_fencing:"https://github.githubassets.com/images/icons/emoji/unicode/1f93a.png?v8", + person_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9bd.png?v8", + person_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9bc.png?v8", + person_red_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b0.png?v8", + person_white_hair:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9b3.png?v8", + person_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f9af.png?v8", + person_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473.png?v8", + peru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ea.png?v8", + petri_dish:"https://github.githubassets.com/images/icons/emoji/unicode/1f9eb.png?v8", + philippines:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1ed.png?v8", + phone:"https://github.githubassets.com/images/icons/emoji/unicode/260e.png?v8", + pick:"https://github.githubassets.com/images/icons/emoji/unicode/26cf.png?v8", + pie:"https://github.githubassets.com/images/icons/emoji/unicode/1f967.png?v8", + pig:"https://github.githubassets.com/images/icons/emoji/unicode/1f437.png?v8", + pig2:"https://github.githubassets.com/images/icons/emoji/unicode/1f416.png?v8", + pig_nose:"https://github.githubassets.com/images/icons/emoji/unicode/1f43d.png?v8", + pill:"https://github.githubassets.com/images/icons/emoji/unicode/1f48a.png?v8", + pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-2708.png?v8", + pinching_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f90f.png?v8", + pineapple:"https://github.githubassets.com/images/icons/emoji/unicode/1f34d.png?v8", + ping_pong:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d3.png?v8", + pirate_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-2620.png?v8", + pisces:"https://github.githubassets.com/images/icons/emoji/unicode/2653.png?v8", + pitcairn_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f3.png?v8", + pizza:"https://github.githubassets.com/images/icons/emoji/unicode/1f355.png?v8", + place_of_worship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d0.png?v8", + plate_with_cutlery:"https://github.githubassets.com/images/icons/emoji/unicode/1f37d.png?v8", + play_or_pause_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ef.png?v8", + pleading_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f97a.png?v8", + point_down:"https://github.githubassets.com/images/icons/emoji/unicode/1f447.png?v8", + point_left:"https://github.githubassets.com/images/icons/emoji/unicode/1f448.png?v8", + point_right:"https://github.githubassets.com/images/icons/emoji/unicode/1f449.png?v8", + point_up:"https://github.githubassets.com/images/icons/emoji/unicode/261d.png?v8", + point_up_2:"https://github.githubassets.com/images/icons/emoji/unicode/1f446.png?v8", + poland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f1.png?v8", + police_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f693.png?v8", + police_officer:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e.png?v8", + policeman:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e-2642.png?v8", + policewoman:"https://github.githubassets.com/images/icons/emoji/unicode/1f46e-2640.png?v8", + poodle:"https://github.githubassets.com/images/icons/emoji/unicode/1f429.png?v8", + poop:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8", + popcorn:"https://github.githubassets.com/images/icons/emoji/unicode/1f37f.png?v8", + portugal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f9.png?v8", + post_office:"https://github.githubassets.com/images/icons/emoji/unicode/1f3e3.png?v8", + postal_horn:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ef.png?v8", + postbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ee.png?v8", + potable_water:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b0.png?v8", + potato:"https://github.githubassets.com/images/icons/emoji/unicode/1f954.png?v8", + pouch:"https://github.githubassets.com/images/icons/emoji/unicode/1f45d.png?v8", + poultry_leg:"https://github.githubassets.com/images/icons/emoji/unicode/1f357.png?v8", + pound:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b7.png?v8", + pout:"https://github.githubassets.com/images/icons/emoji/unicode/1f621.png?v8", + pouting_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63e.png?v8", + pouting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e.png?v8", + pouting_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e-2642.png?v8", + pouting_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64e-2640.png?v8", + pray:"https://github.githubassets.com/images/icons/emoji/unicode/1f64f.png?v8", + prayer_beads:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ff.png?v8", + pregnant_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f930.png?v8", + pretzel:"https://github.githubassets.com/images/icons/emoji/unicode/1f968.png?v8", + previous_track_button:"https://github.githubassets.com/images/icons/emoji/unicode/23ee.png?v8", + prince:"https://github.githubassets.com/images/icons/emoji/unicode/1f934.png?v8", + princess:"https://github.githubassets.com/images/icons/emoji/unicode/1f478.png?v8", + printer:"https://github.githubassets.com/images/icons/emoji/unicode/1f5a8.png?v8", + probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f9af.png?v8", + puerto_rico:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f7.png?v8", + punch:"https://github.githubassets.com/images/icons/emoji/unicode/1f44a.png?v8", + purple_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e3.png?v8", + purple_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49c.png?v8", + purple_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7ea.png?v8", + purse:"https://github.githubassets.com/images/icons/emoji/unicode/1f45b.png?v8", + pushpin:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cc.png?v8", + put_litter_in_its_place:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ae.png?v8", + qatar:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f6-1f1e6.png?v8", + question:"https://github.githubassets.com/images/icons/emoji/unicode/2753.png?v8", + rabbit:"https://github.githubassets.com/images/icons/emoji/unicode/1f430.png?v8", + rabbit2:"https://github.githubassets.com/images/icons/emoji/unicode/1f407.png?v8", + raccoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f99d.png?v8", + racehorse:"https://github.githubassets.com/images/icons/emoji/unicode/1f40e.png?v8", + racing_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ce.png?v8", + radio:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fb.png?v8", + radio_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f518.png?v8", + radioactive:"https://github.githubassets.com/images/icons/emoji/unicode/2622.png?v8", + rage:"https://github.githubassets.com/images/icons/emoji/unicode/1f621.png?v8", + rage1:"https://github.githubassets.com/images/icons/emoji/rage1.png?v8", + rage2:"https://github.githubassets.com/images/icons/emoji/rage2.png?v8", + rage3:"https://github.githubassets.com/images/icons/emoji/rage3.png?v8", + rage4:"https://github.githubassets.com/images/icons/emoji/rage4.png?v8", + railway_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f683.png?v8", + railway_track:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e4.png?v8", + rainbow:"https://github.githubassets.com/images/icons/emoji/unicode/1f308.png?v8", + rainbow_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f3-1f308.png?v8", + raised_back_of_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f91a.png?v8", + raised_eyebrow:"https://github.githubassets.com/images/icons/emoji/unicode/1f928.png?v8", + raised_hand:"https://github.githubassets.com/images/icons/emoji/unicode/270b.png?v8", + raised_hand_with_fingers_splayed:"https://github.githubassets.com/images/icons/emoji/unicode/1f590.png?v8", + raised_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f64c.png?v8", + raising_hand:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b.png?v8", + raising_hand_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b-2642.png?v8", + raising_hand_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f64b-2640.png?v8", + ram:"https://github.githubassets.com/images/icons/emoji/unicode/1f40f.png?v8", + ramen:"https://github.githubassets.com/images/icons/emoji/unicode/1f35c.png?v8", + rat:"https://github.githubassets.com/images/icons/emoji/unicode/1f400.png?v8", + razor:"https://github.githubassets.com/images/icons/emoji/unicode/1fa92.png?v8", + receipt:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fe.png?v8", + record_button:"https://github.githubassets.com/images/icons/emoji/unicode/23fa.png?v8", + recycle:"https://github.githubassets.com/images/icons/emoji/unicode/267b.png?v8", + red_car:"https://github.githubassets.com/images/icons/emoji/unicode/1f697.png?v8", + red_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f534.png?v8", + red_envelope:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e7.png?v8", + red_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b0.png?v8", + red_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b0.png?v8", + red_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e5.png?v8", + registered:"https://github.githubassets.com/images/icons/emoji/unicode/00ae.png?v8", + relaxed:"https://github.githubassets.com/images/icons/emoji/unicode/263a.png?v8", + relieved:"https://github.githubassets.com/images/icons/emoji/unicode/1f60c.png?v8", + reminder_ribbon:"https://github.githubassets.com/images/icons/emoji/unicode/1f397.png?v8", + repeat:"https://github.githubassets.com/images/icons/emoji/unicode/1f501.png?v8", + repeat_one:"https://github.githubassets.com/images/icons/emoji/unicode/1f502.png?v8", + rescue_worker_helmet:"https://github.githubassets.com/images/icons/emoji/unicode/26d1.png?v8", + restroom:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bb.png?v8", + reunion:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1ea.png?v8", + revolving_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f49e.png?v8", + rewind:"https://github.githubassets.com/images/icons/emoji/unicode/23ea.png?v8", + rhinoceros:"https://github.githubassets.com/images/icons/emoji/unicode/1f98f.png?v8", + ribbon:"https://github.githubassets.com/images/icons/emoji/unicode/1f380.png?v8", + rice:"https://github.githubassets.com/images/icons/emoji/unicode/1f35a.png?v8", + rice_ball:"https://github.githubassets.com/images/icons/emoji/unicode/1f359.png?v8", + rice_cracker:"https://github.githubassets.com/images/icons/emoji/unicode/1f358.png?v8", + rice_scene:"https://github.githubassets.com/images/icons/emoji/unicode/1f391.png?v8", + right_anger_bubble:"https://github.githubassets.com/images/icons/emoji/unicode/1f5ef.png?v8", + ring:"https://github.githubassets.com/images/icons/emoji/unicode/1f48d.png?v8", + ringed_planet:"https://github.githubassets.com/images/icons/emoji/unicode/1fa90.png?v8", + robot:"https://github.githubassets.com/images/icons/emoji/unicode/1f916.png?v8", + rocket:"https://github.githubassets.com/images/icons/emoji/unicode/1f680.png?v8", + rofl:"https://github.githubassets.com/images/icons/emoji/unicode/1f923.png?v8", + roll_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f644.png?v8", + roll_of_paper:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fb.png?v8", + roller_coaster:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a2.png?v8", + romania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1f4.png?v8", + rooster:"https://github.githubassets.com/images/icons/emoji/unicode/1f413.png?v8", + rose:"https://github.githubassets.com/images/icons/emoji/unicode/1f339.png?v8", + rosette:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f5.png?v8", + rotating_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a8.png?v8", + round_pushpin:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cd.png?v8", + rowboat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3.png?v8", + rowing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3-2642.png?v8", + rowing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a3-2640.png?v8", + ru:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1fa.png?v8", + rugby_football:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c9.png?v8", + runner:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3.png?v8", + running:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3.png?v8", + running_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3-2642.png?v8", + running_shirt_with_sash:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bd.png?v8", + running_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c3-2640.png?v8", + rwanda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1fc.png?v8", + sa:"https://github.githubassets.com/images/icons/emoji/unicode/1f202.png?v8", + safety_pin:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f7.png?v8", + safety_vest:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ba.png?v8", + sagittarius:"https://github.githubassets.com/images/icons/emoji/unicode/2650.png?v8", + sailboat:"https://github.githubassets.com/images/icons/emoji/unicode/26f5.png?v8", + sake:"https://github.githubassets.com/images/icons/emoji/unicode/1f376.png?v8", + salt:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c2.png?v8", + samoa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fc-1f1f8.png?v8", + san_marino:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f2.png?v8", + sandal:"https://github.githubassets.com/images/icons/emoji/unicode/1f461.png?v8", + sandwich:"https://github.githubassets.com/images/icons/emoji/unicode/1f96a.png?v8", + santa:"https://github.githubassets.com/images/icons/emoji/unicode/1f385.png?v8", + sao_tome_principe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f9.png?v8", + sari:"https://github.githubassets.com/images/icons/emoji/unicode/1f97b.png?v8", + sassy_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2642.png?v8", + sassy_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2640.png?v8", + satellite:"https://github.githubassets.com/images/icons/emoji/unicode/1f4e1.png?v8", + satisfied:"https://github.githubassets.com/images/icons/emoji/unicode/1f606.png?v8", + saudi_arabia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e6.png?v8", + sauna_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6-2642.png?v8", + sauna_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6.png?v8", + sauna_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d6-2640.png?v8", + sauropod:"https://github.githubassets.com/images/icons/emoji/unicode/1f995.png?v8", + saxophone:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b7.png?v8", + scarf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e3.png?v8", + school:"https://github.githubassets.com/images/icons/emoji/unicode/1f3eb.png?v8", + school_satchel:"https://github.githubassets.com/images/icons/emoji/unicode/1f392.png?v8", + scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f52c.png?v8", + scissors:"https://github.githubassets.com/images/icons/emoji/unicode/2702.png?v8", + scorpion:"https://github.githubassets.com/images/icons/emoji/unicode/1f982.png?v8", + scorpius:"https://github.githubassets.com/images/icons/emoji/unicode/264f.png?v8", + scotland:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0073-e0063-e0074-e007f.png?v8", + scream:"https://github.githubassets.com/images/icons/emoji/unicode/1f631.png?v8", + scream_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f640.png?v8", + scroll:"https://github.githubassets.com/images/icons/emoji/unicode/1f4dc.png?v8", + seat:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ba.png?v8", + secret:"https://github.githubassets.com/images/icons/emoji/unicode/3299.png?v8", + see_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f648.png?v8", + seedling:"https://github.githubassets.com/images/icons/emoji/unicode/1f331.png?v8", + selfie:"https://github.githubassets.com/images/icons/emoji/unicode/1f933.png?v8", + senegal:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f3.png?v8", + serbia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f7-1f1f8.png?v8", + service_dog:"https://github.githubassets.com/images/icons/emoji/unicode/1f415-1f9ba.png?v8", + seven:"https://github.githubassets.com/images/icons/emoji/unicode/0037-20e3.png?v8", + seychelles:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e8.png?v8", + shallow_pan_of_food:"https://github.githubassets.com/images/icons/emoji/unicode/1f958.png?v8", + shamrock:"https://github.githubassets.com/images/icons/emoji/unicode/2618.png?v8", + shark:"https://github.githubassets.com/images/icons/emoji/unicode/1f988.png?v8", + shaved_ice:"https://github.githubassets.com/images/icons/emoji/unicode/1f367.png?v8", + sheep:"https://github.githubassets.com/images/icons/emoji/unicode/1f411.png?v8", + shell:"https://github.githubassets.com/images/icons/emoji/unicode/1f41a.png?v8", + shield:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e1.png?v8", + shinto_shrine:"https://github.githubassets.com/images/icons/emoji/unicode/26e9.png?v8", + ship:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a2.png?v8", + shipit:"https://github.githubassets.com/images/icons/emoji/shipit.png?v8", + shirt:"https://github.githubassets.com/images/icons/emoji/unicode/1f455.png?v8", + shit:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a9.png?v8", + shoe:"https://github.githubassets.com/images/icons/emoji/unicode/1f45e.png?v8", + shopping:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cd.png?v8", + shopping_cart:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d2.png?v8", + shorts:"https://github.githubassets.com/images/icons/emoji/unicode/1fa73.png?v8", + shower:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bf.png?v8", + shrimp:"https://github.githubassets.com/images/icons/emoji/unicode/1f990.png?v8", + shrug:"https://github.githubassets.com/images/icons/emoji/unicode/1f937.png?v8", + shushing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92b.png?v8", + sierra_leone:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f1.png?v8", + signal_strength:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f6.png?v8", + singapore:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ec.png?v8", + singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3a4.png?v8", + sint_maarten:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fd.png?v8", + six:"https://github.githubassets.com/images/icons/emoji/unicode/0036-20e3.png?v8", + six_pointed_star:"https://github.githubassets.com/images/icons/emoji/unicode/1f52f.png?v8", + skateboard:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f9.png?v8", + ski:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bf.png?v8", + skier:"https://github.githubassets.com/images/icons/emoji/unicode/26f7.png?v8", + skull:"https://github.githubassets.com/images/icons/emoji/unicode/1f480.png?v8", + skull_and_crossbones:"https://github.githubassets.com/images/icons/emoji/unicode/2620.png?v8", + skunk:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a8.png?v8", + sled:"https://github.githubassets.com/images/icons/emoji/unicode/1f6f7.png?v8", + sleeping:"https://github.githubassets.com/images/icons/emoji/unicode/1f634.png?v8", + sleeping_bed:"https://github.githubassets.com/images/icons/emoji/unicode/1f6cc.png?v8", + sleepy:"https://github.githubassets.com/images/icons/emoji/unicode/1f62a.png?v8", + slightly_frowning_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f641.png?v8", + slightly_smiling_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f642.png?v8", + slot_machine:"https://github.githubassets.com/images/icons/emoji/unicode/1f3b0.png?v8", + sloth:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a5.png?v8", + slovakia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f0.png?v8", + slovenia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ee.png?v8", + small_airplane:"https://github.githubassets.com/images/icons/emoji/unicode/1f6e9.png?v8", + small_blue_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f539.png?v8", + small_orange_diamond:"https://github.githubassets.com/images/icons/emoji/unicode/1f538.png?v8", + small_red_triangle:"https://github.githubassets.com/images/icons/emoji/unicode/1f53a.png?v8", + small_red_triangle_down:"https://github.githubassets.com/images/icons/emoji/unicode/1f53b.png?v8", + smile:"https://github.githubassets.com/images/icons/emoji/unicode/1f604.png?v8", + smile_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f638.png?v8", + smiley:"https://github.githubassets.com/images/icons/emoji/unicode/1f603.png?v8", + smiley_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63a.png?v8", + smiling_face_with_three_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f970.png?v8", + smiling_imp:"https://github.githubassets.com/images/icons/emoji/unicode/1f608.png?v8", + smirk:"https://github.githubassets.com/images/icons/emoji/unicode/1f60f.png?v8", + smirk_cat:"https://github.githubassets.com/images/icons/emoji/unicode/1f63c.png?v8", + smoking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ac.png?v8", + snail:"https://github.githubassets.com/images/icons/emoji/unicode/1f40c.png?v8", + snake:"https://github.githubassets.com/images/icons/emoji/unicode/1f40d.png?v8", + sneezing_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f927.png?v8", + snowboarder:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c2.png?v8", + snowflake:"https://github.githubassets.com/images/icons/emoji/unicode/2744.png?v8", + snowman:"https://github.githubassets.com/images/icons/emoji/unicode/26c4.png?v8", + snowman_with_snow:"https://github.githubassets.com/images/icons/emoji/unicode/2603.png?v8", + soap:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fc.png?v8", + sob:"https://github.githubassets.com/images/icons/emoji/unicode/1f62d.png?v8", + soccer:"https://github.githubassets.com/images/icons/emoji/unicode/26bd.png?v8", + socks:"https://github.githubassets.com/images/icons/emoji/unicode/1f9e6.png?v8", + softball:"https://github.githubassets.com/images/icons/emoji/unicode/1f94e.png?v8", + solomon_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e7.png?v8", + somalia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f4.png?v8", + soon:"https://github.githubassets.com/images/icons/emoji/unicode/1f51c.png?v8", + sos:"https://github.githubassets.com/images/icons/emoji/unicode/1f198.png?v8", + sound:"https://github.githubassets.com/images/icons/emoji/unicode/1f509.png?v8", + south_africa:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1e6.png?v8", + south_georgia_south_sandwich_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1f8.png?v8", + south_sudan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f8.png?v8", + space_invader:"https://github.githubassets.com/images/icons/emoji/unicode/1f47e.png?v8", + spades:"https://github.githubassets.com/images/icons/emoji/unicode/2660.png?v8", + spaghetti:"https://github.githubassets.com/images/icons/emoji/unicode/1f35d.png?v8", + sparkle:"https://github.githubassets.com/images/icons/emoji/unicode/2747.png?v8", + sparkler:"https://github.githubassets.com/images/icons/emoji/unicode/1f387.png?v8", + sparkles:"https://github.githubassets.com/images/icons/emoji/unicode/2728.png?v8", + sparkling_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f496.png?v8", + speak_no_evil:"https://github.githubassets.com/images/icons/emoji/unicode/1f64a.png?v8", + speaker:"https://github.githubassets.com/images/icons/emoji/unicode/1f508.png?v8", + speaking_head:"https://github.githubassets.com/images/icons/emoji/unicode/1f5e3.png?v8", + speech_balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ac.png?v8", + speedboat:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a4.png?v8", + spider:"https://github.githubassets.com/images/icons/emoji/unicode/1f577.png?v8", + spider_web:"https://github.githubassets.com/images/icons/emoji/unicode/1f578.png?v8", + spiral_calendar:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d3.png?v8", + spiral_notepad:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d2.png?v8", + sponge:"https://github.githubassets.com/images/icons/emoji/unicode/1f9fd.png?v8", + spoon:"https://github.githubassets.com/images/icons/emoji/unicode/1f944.png?v8", + squid:"https://github.githubassets.com/images/icons/emoji/unicode/1f991.png?v8", + sri_lanka:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1f0.png?v8", + st_barthelemy:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e7-1f1f1.png?v8", + st_helena:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ed.png?v8", + st_kitts_nevis:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f0-1f1f3.png?v8", + st_lucia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f1-1f1e8.png?v8", + st_martin:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f2-1f1eb.png?v8", + st_pierre_miquelon:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f5-1f1f2.png?v8", + st_vincent_grenadines:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1e8.png?v8", + stadium:"https://github.githubassets.com/images/icons/emoji/unicode/1f3df.png?v8", + standing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd-2642.png?v8", + standing_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd.png?v8", + standing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9cd-2640.png?v8", + star:"https://github.githubassets.com/images/icons/emoji/unicode/2b50.png?v8", + star2:"https://github.githubassets.com/images/icons/emoji/unicode/1f31f.png?v8", + star_and_crescent:"https://github.githubassets.com/images/icons/emoji/unicode/262a.png?v8", + star_of_david:"https://github.githubassets.com/images/icons/emoji/unicode/2721.png?v8", + star_struck:"https://github.githubassets.com/images/icons/emoji/unicode/1f929.png?v8", + stars:"https://github.githubassets.com/images/icons/emoji/unicode/1f320.png?v8", + station:"https://github.githubassets.com/images/icons/emoji/unicode/1f689.png?v8", + statue_of_liberty:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fd.png?v8", + steam_locomotive:"https://github.githubassets.com/images/icons/emoji/unicode/1f682.png?v8", + stethoscope:"https://github.githubassets.com/images/icons/emoji/unicode/1fa7a.png?v8", + stew:"https://github.githubassets.com/images/icons/emoji/unicode/1f372.png?v8", + stop_button:"https://github.githubassets.com/images/icons/emoji/unicode/23f9.png?v8", + stop_sign:"https://github.githubassets.com/images/icons/emoji/unicode/1f6d1.png?v8", + stopwatch:"https://github.githubassets.com/images/icons/emoji/unicode/23f1.png?v8", + straight_ruler:"https://github.githubassets.com/images/icons/emoji/unicode/1f4cf.png?v8", + strawberry:"https://github.githubassets.com/images/icons/emoji/unicode/1f353.png?v8", + stuck_out_tongue:"https://github.githubassets.com/images/icons/emoji/unicode/1f61b.png?v8", + stuck_out_tongue_closed_eyes:"https://github.githubassets.com/images/icons/emoji/unicode/1f61d.png?v8", + stuck_out_tongue_winking_eye:"https://github.githubassets.com/images/icons/emoji/unicode/1f61c.png?v8", + student:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f393.png?v8", + studio_microphone:"https://github.githubassets.com/images/icons/emoji/unicode/1f399.png?v8", + stuffed_flatbread:"https://github.githubassets.com/images/icons/emoji/unicode/1f959.png?v8", + sudan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1e9.png?v8", + sun_behind_large_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f325.png?v8", + sun_behind_rain_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f326.png?v8", + sun_behind_small_cloud:"https://github.githubassets.com/images/icons/emoji/unicode/1f324.png?v8", + sun_with_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f31e.png?v8", + sunflower:"https://github.githubassets.com/images/icons/emoji/unicode/1f33b.png?v8", + sunglasses:"https://github.githubassets.com/images/icons/emoji/unicode/1f60e.png?v8", + sunny:"https://github.githubassets.com/images/icons/emoji/unicode/2600.png?v8", + sunrise:"https://github.githubassets.com/images/icons/emoji/unicode/1f305.png?v8", + sunrise_over_mountains:"https://github.githubassets.com/images/icons/emoji/unicode/1f304.png?v8", + superhero:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8.png?v8", + superhero_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8-2642.png?v8", + superhero_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b8-2640.png?v8", + supervillain:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9.png?v8", + supervillain_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9-2642.png?v8", + supervillain_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b9-2640.png?v8", + surfer:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4.png?v8", + surfing_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4-2642.png?v8", + surfing_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c4-2640.png?v8", + suriname:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1f7.png?v8", + sushi:"https://github.githubassets.com/images/icons/emoji/unicode/1f363.png?v8", + suspect:"https://github.githubassets.com/images/icons/emoji/suspect.png?v8", + suspension_railway:"https://github.githubassets.com/images/icons/emoji/unicode/1f69f.png?v8", + svalbard_jan_mayen:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ef.png?v8", + swan:"https://github.githubassets.com/images/icons/emoji/unicode/1f9a2.png?v8", + swaziland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ff.png?v8", + sweat:"https://github.githubassets.com/images/icons/emoji/unicode/1f613.png?v8", + sweat_drops:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a6.png?v8", + sweat_smile:"https://github.githubassets.com/images/icons/emoji/unicode/1f605.png?v8", + sweden:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1ea.png?v8", + sweet_potato:"https://github.githubassets.com/images/icons/emoji/unicode/1f360.png?v8", + swim_brief:"https://github.githubassets.com/images/icons/emoji/unicode/1fa72.png?v8", + swimmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca.png?v8", + swimming_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca-2642.png?v8", + swimming_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ca-2640.png?v8", + switzerland:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e8-1f1ed.png?v8", + symbols:"https://github.githubassets.com/images/icons/emoji/unicode/1f523.png?v8", + synagogue:"https://github.githubassets.com/images/icons/emoji/unicode/1f54d.png?v8", + syria:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f8-1f1fe.png?v8", + syringe:"https://github.githubassets.com/images/icons/emoji/unicode/1f489.png?v8", + "t-rex":"https://github.githubassets.com/images/icons/emoji/unicode/1f996.png?v8", + taco:"https://github.githubassets.com/images/icons/emoji/unicode/1f32e.png?v8", + tada:"https://github.githubassets.com/images/icons/emoji/unicode/1f389.png?v8", + taiwan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1fc.png?v8", + tajikistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ef.png?v8", + takeout_box:"https://github.githubassets.com/images/icons/emoji/unicode/1f961.png?v8", + tanabata_tree:"https://github.githubassets.com/images/icons/emoji/unicode/1f38b.png?v8", + tangerine:"https://github.githubassets.com/images/icons/emoji/unicode/1f34a.png?v8", + tanzania:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ff.png?v8", + taurus:"https://github.githubassets.com/images/icons/emoji/unicode/2649.png?v8", + taxi:"https://github.githubassets.com/images/icons/emoji/unicode/1f695.png?v8", + tea:"https://github.githubassets.com/images/icons/emoji/unicode/1f375.png?v8", + teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f3eb.png?v8", + technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d1-1f4bb.png?v8", + teddy_bear:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f8.png?v8", + telephone:"https://github.githubassets.com/images/icons/emoji/unicode/260e.png?v8", + telephone_receiver:"https://github.githubassets.com/images/icons/emoji/unicode/1f4de.png?v8", + telescope:"https://github.githubassets.com/images/icons/emoji/unicode/1f52d.png?v8", + tennis:"https://github.githubassets.com/images/icons/emoji/unicode/1f3be.png?v8", + tent:"https://github.githubassets.com/images/icons/emoji/unicode/26fa.png?v8", + test_tube:"https://github.githubassets.com/images/icons/emoji/unicode/1f9ea.png?v8", + thailand:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ed.png?v8", + thermometer:"https://github.githubassets.com/images/icons/emoji/unicode/1f321.png?v8", + thinking:"https://github.githubassets.com/images/icons/emoji/unicode/1f914.png?v8", + thought_balloon:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ad.png?v8", + thread:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f5.png?v8", + three:"https://github.githubassets.com/images/icons/emoji/unicode/0033-20e3.png?v8", + thumbsdown:"https://github.githubassets.com/images/icons/emoji/unicode/1f44e.png?v8", + thumbsup:"https://github.githubassets.com/images/icons/emoji/unicode/1f44d.png?v8", + ticket:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ab.png?v8", + tickets:"https://github.githubassets.com/images/icons/emoji/unicode/1f39f.png?v8", + tiger:"https://github.githubassets.com/images/icons/emoji/unicode/1f42f.png?v8", + tiger2:"https://github.githubassets.com/images/icons/emoji/unicode/1f405.png?v8", + timer_clock:"https://github.githubassets.com/images/icons/emoji/unicode/23f2.png?v8", + timor_leste:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f1.png?v8", + tipping_hand_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2642.png?v8", + tipping_hand_person:"https://github.githubassets.com/images/icons/emoji/unicode/1f481.png?v8", + tipping_hand_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f481-2640.png?v8", + tired_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f62b.png?v8", + tm:"https://github.githubassets.com/images/icons/emoji/unicode/2122.png?v8", + togo:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1ec.png?v8", + toilet:"https://github.githubassets.com/images/icons/emoji/unicode/1f6bd.png?v8", + tokelau:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f0.png?v8", + tokyo_tower:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fc.png?v8", + tomato:"https://github.githubassets.com/images/icons/emoji/unicode/1f345.png?v8", + tonga:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f4.png?v8", + tongue:"https://github.githubassets.com/images/icons/emoji/unicode/1f445.png?v8", + toolbox:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f0.png?v8", + tooth:"https://github.githubassets.com/images/icons/emoji/unicode/1f9b7.png?v8", + top:"https://github.githubassets.com/images/icons/emoji/unicode/1f51d.png?v8", + tophat:"https://github.githubassets.com/images/icons/emoji/unicode/1f3a9.png?v8", + tornado:"https://github.githubassets.com/images/icons/emoji/unicode/1f32a.png?v8", + tr:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f7.png?v8", + trackball:"https://github.githubassets.com/images/icons/emoji/unicode/1f5b2.png?v8", + tractor:"https://github.githubassets.com/images/icons/emoji/unicode/1f69c.png?v8", + traffic_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a5.png?v8", + train:"https://github.githubassets.com/images/icons/emoji/unicode/1f68b.png?v8", + train2:"https://github.githubassets.com/images/icons/emoji/unicode/1f686.png?v8", + tram:"https://github.githubassets.com/images/icons/emoji/unicode/1f68a.png?v8", + triangular_flag_on_post:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a9.png?v8", + triangular_ruler:"https://github.githubassets.com/images/icons/emoji/unicode/1f4d0.png?v8", + trident:"https://github.githubassets.com/images/icons/emoji/unicode/1f531.png?v8", + trinidad_tobago:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f9.png?v8", + tristan_da_cunha:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e6.png?v8", + triumph:"https://github.githubassets.com/images/icons/emoji/unicode/1f624.png?v8", + trolleybus:"https://github.githubassets.com/images/icons/emoji/unicode/1f68e.png?v8", + trollface:"https://github.githubassets.com/images/icons/emoji/trollface.png?v8", + trophy:"https://github.githubassets.com/images/icons/emoji/unicode/1f3c6.png?v8", + tropical_drink:"https://github.githubassets.com/images/icons/emoji/unicode/1f379.png?v8", + tropical_fish:"https://github.githubassets.com/images/icons/emoji/unicode/1f420.png?v8", + truck:"https://github.githubassets.com/images/icons/emoji/unicode/1f69a.png?v8", + trumpet:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ba.png?v8", + tshirt:"https://github.githubassets.com/images/icons/emoji/unicode/1f455.png?v8", + tulip:"https://github.githubassets.com/images/icons/emoji/unicode/1f337.png?v8", + tumbler_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f943.png?v8", + tunisia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f3.png?v8", + turkey:"https://github.githubassets.com/images/icons/emoji/unicode/1f983.png?v8", + turkmenistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1f2.png?v8", + turks_caicos_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1e8.png?v8", + turtle:"https://github.githubassets.com/images/icons/emoji/unicode/1f422.png?v8", + tuvalu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1f9-1f1fb.png?v8", + tv:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fa.png?v8", + twisted_rightwards_arrows:"https://github.githubassets.com/images/icons/emoji/unicode/1f500.png?v8", + two:"https://github.githubassets.com/images/icons/emoji/unicode/0032-20e3.png?v8", + two_hearts:"https://github.githubassets.com/images/icons/emoji/unicode/1f495.png?v8", + two_men_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f46c.png?v8", + two_women_holding_hands:"https://github.githubassets.com/images/icons/emoji/unicode/1f46d.png?v8", + u5272:"https://github.githubassets.com/images/icons/emoji/unicode/1f239.png?v8", + u5408:"https://github.githubassets.com/images/icons/emoji/unicode/1f234.png?v8", + u55b6:"https://github.githubassets.com/images/icons/emoji/unicode/1f23a.png?v8", + u6307:"https://github.githubassets.com/images/icons/emoji/unicode/1f22f.png?v8", + u6708:"https://github.githubassets.com/images/icons/emoji/unicode/1f237.png?v8", + u6709:"https://github.githubassets.com/images/icons/emoji/unicode/1f236.png?v8", + u6e80:"https://github.githubassets.com/images/icons/emoji/unicode/1f235.png?v8", + u7121:"https://github.githubassets.com/images/icons/emoji/unicode/1f21a.png?v8", + u7533:"https://github.githubassets.com/images/icons/emoji/unicode/1f238.png?v8", + u7981:"https://github.githubassets.com/images/icons/emoji/unicode/1f232.png?v8", + u7a7a:"https://github.githubassets.com/images/icons/emoji/unicode/1f233.png?v8", + uganda:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1ec.png?v8", + uk:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ec-1f1e7.png?v8", + ukraine:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1e6.png?v8", + umbrella:"https://github.githubassets.com/images/icons/emoji/unicode/2614.png?v8", + unamused:"https://github.githubassets.com/images/icons/emoji/unicode/1f612.png?v8", + underage:"https://github.githubassets.com/images/icons/emoji/unicode/1f51e.png?v8", + unicorn:"https://github.githubassets.com/images/icons/emoji/unicode/1f984.png?v8", + united_arab_emirates:"https://github.githubassets.com/images/icons/emoji/unicode/1f1e6-1f1ea.png?v8", + united_nations:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f3.png?v8", + unlock:"https://github.githubassets.com/images/icons/emoji/unicode/1f513.png?v8", + up:"https://github.githubassets.com/images/icons/emoji/unicode/1f199.png?v8", + upside_down_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f643.png?v8", + uruguay:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1fe.png?v8", + us:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f8.png?v8", + us_outlying_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1f2.png?v8", + us_virgin_islands:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ee.png?v8", + uzbekistan:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fa-1f1ff.png?v8", + v:"https://github.githubassets.com/images/icons/emoji/unicode/270c.png?v8", + vampire:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db.png?v8", + vampire_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db-2642.png?v8", + vampire_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9db-2640.png?v8", + vanuatu:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1fa.png?v8", + vatican_city:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1e6.png?v8", + venezuela:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1ea.png?v8", + vertical_traffic_light:"https://github.githubassets.com/images/icons/emoji/unicode/1f6a6.png?v8", + vhs:"https://github.githubassets.com/images/icons/emoji/unicode/1f4fc.png?v8", + vibration_mode:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f3.png?v8", + video_camera:"https://github.githubassets.com/images/icons/emoji/unicode/1f4f9.png?v8", + video_game:"https://github.githubassets.com/images/icons/emoji/unicode/1f3ae.png?v8", + vietnam:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fb-1f1f3.png?v8", + violin:"https://github.githubassets.com/images/icons/emoji/unicode/1f3bb.png?v8", + virgo:"https://github.githubassets.com/images/icons/emoji/unicode/264d.png?v8", + volcano:"https://github.githubassets.com/images/icons/emoji/unicode/1f30b.png?v8", + volleyball:"https://github.githubassets.com/images/icons/emoji/unicode/1f3d0.png?v8", + vomiting_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92e.png?v8", + vs:"https://github.githubassets.com/images/icons/emoji/unicode/1f19a.png?v8", + vulcan_salute:"https://github.githubassets.com/images/icons/emoji/unicode/1f596.png?v8", + waffle:"https://github.githubassets.com/images/icons/emoji/unicode/1f9c7.png?v8", + wales:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f4-e0067-e0062-e0077-e006c-e0073-e007f.png?v8", + walking:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6.png?v8", + walking_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6-2642.png?v8", + walking_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f6b6-2640.png?v8", + wallis_futuna:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fc-1f1eb.png?v8", + waning_crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f318.png?v8", + waning_gibbous_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f316.png?v8", + warning:"https://github.githubassets.com/images/icons/emoji/unicode/26a0.png?v8", + wastebasket:"https://github.githubassets.com/images/icons/emoji/unicode/1f5d1.png?v8", + watch:"https://github.githubassets.com/images/icons/emoji/unicode/231a.png?v8", + water_buffalo:"https://github.githubassets.com/images/icons/emoji/unicode/1f403.png?v8", + water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d.png?v8", + watermelon:"https://github.githubassets.com/images/icons/emoji/unicode/1f349.png?v8", + wave:"https://github.githubassets.com/images/icons/emoji/unicode/1f44b.png?v8", + wavy_dash:"https://github.githubassets.com/images/icons/emoji/unicode/3030.png?v8", + waxing_crescent_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f312.png?v8", + waxing_gibbous_moon:"https://github.githubassets.com/images/icons/emoji/unicode/1f314.png?v8", + wc:"https://github.githubassets.com/images/icons/emoji/unicode/1f6be.png?v8", + weary:"https://github.githubassets.com/images/icons/emoji/unicode/1f629.png?v8", + wedding:"https://github.githubassets.com/images/icons/emoji/unicode/1f492.png?v8", + weight_lifting:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb.png?v8", + weight_lifting_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb-2642.png?v8", + weight_lifting_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f3cb-2640.png?v8", + western_sahara:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ea-1f1ed.png?v8", + whale:"https://github.githubassets.com/images/icons/emoji/unicode/1f433.png?v8", + whale2:"https://github.githubassets.com/images/icons/emoji/unicode/1f40b.png?v8", + wheel_of_dharma:"https://github.githubassets.com/images/icons/emoji/unicode/2638.png?v8", + wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/267f.png?v8", + white_check_mark:"https://github.githubassets.com/images/icons/emoji/unicode/2705.png?v8", + white_circle:"https://github.githubassets.com/images/icons/emoji/unicode/26aa.png?v8", + white_flag:"https://github.githubassets.com/images/icons/emoji/unicode/1f3f3.png?v8", + white_flower:"https://github.githubassets.com/images/icons/emoji/unicode/1f4ae.png?v8", + white_haired_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f468-1f9b3.png?v8", + white_haired_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9b3.png?v8", + white_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f90d.png?v8", + white_large_square:"https://github.githubassets.com/images/icons/emoji/unicode/2b1c.png?v8", + white_medium_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fd.png?v8", + white_medium_square:"https://github.githubassets.com/images/icons/emoji/unicode/25fb.png?v8", + white_small_square:"https://github.githubassets.com/images/icons/emoji/unicode/25ab.png?v8", + white_square_button:"https://github.githubassets.com/images/icons/emoji/unicode/1f533.png?v8", + wilted_flower:"https://github.githubassets.com/images/icons/emoji/unicode/1f940.png?v8", + wind_chime:"https://github.githubassets.com/images/icons/emoji/unicode/1f390.png?v8", + wind_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f32c.png?v8", + wine_glass:"https://github.githubassets.com/images/icons/emoji/unicode/1f377.png?v8", + wink:"https://github.githubassets.com/images/icons/emoji/unicode/1f609.png?v8", + wolf:"https://github.githubassets.com/images/icons/emoji/unicode/1f43a.png?v8", + woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f469.png?v8", + woman_artist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3a8.png?v8", + woman_astronaut:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f680.png?v8", + woman_cartwheeling:"https://github.githubassets.com/images/icons/emoji/unicode/1f938-2640.png?v8", + woman_cook:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f373.png?v8", + woman_dancing:"https://github.githubassets.com/images/icons/emoji/unicode/1f483.png?v8", + woman_facepalming:"https://github.githubassets.com/images/icons/emoji/unicode/1f926-2640.png?v8", + woman_factory_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3ed.png?v8", + woman_farmer:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f33e.png?v8", + woman_firefighter:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f692.png?v8", + woman_health_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2695.png?v8", + woman_in_manual_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9bd.png?v8", + woman_in_motorized_wheelchair:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9bc.png?v8", + woman_judge:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2696.png?v8", + woman_juggling:"https://github.githubassets.com/images/icons/emoji/unicode/1f939-2640.png?v8", + woman_mechanic:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f527.png?v8", + woman_office_worker:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f4bc.png?v8", + woman_pilot:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-2708.png?v8", + woman_playing_handball:"https://github.githubassets.com/images/icons/emoji/unicode/1f93e-2640.png?v8", + woman_playing_water_polo:"https://github.githubassets.com/images/icons/emoji/unicode/1f93d-2640.png?v8", + woman_scientist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f52c.png?v8", + woman_shrugging:"https://github.githubassets.com/images/icons/emoji/unicode/1f937-2640.png?v8", + woman_singer:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3a4.png?v8", + woman_student:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f393.png?v8", + woman_teacher:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f3eb.png?v8", + woman_technologist:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f4bb.png?v8", + woman_with_headscarf:"https://github.githubassets.com/images/icons/emoji/unicode/1f9d5.png?v8", + woman_with_probing_cane:"https://github.githubassets.com/images/icons/emoji/unicode/1f469-1f9af.png?v8", + woman_with_turban:"https://github.githubassets.com/images/icons/emoji/unicode/1f473-2640.png?v8", + womans_clothes:"https://github.githubassets.com/images/icons/emoji/unicode/1f45a.png?v8", + womans_hat:"https://github.githubassets.com/images/icons/emoji/unicode/1f452.png?v8", + women_wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c-2640.png?v8", + womens:"https://github.githubassets.com/images/icons/emoji/unicode/1f6ba.png?v8", + woozy_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f974.png?v8", + world_map:"https://github.githubassets.com/images/icons/emoji/unicode/1f5fa.png?v8", + worried:"https://github.githubassets.com/images/icons/emoji/unicode/1f61f.png?v8", + wrench:"https://github.githubassets.com/images/icons/emoji/unicode/1f527.png?v8", + wrestling:"https://github.githubassets.com/images/icons/emoji/unicode/1f93c.png?v8", + writing_hand:"https://github.githubassets.com/images/icons/emoji/unicode/270d.png?v8", + x:"https://github.githubassets.com/images/icons/emoji/unicode/274c.png?v8", + yarn:"https://github.githubassets.com/images/icons/emoji/unicode/1f9f6.png?v8", + yawning_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f971.png?v8", + yellow_circle:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e1.png?v8", + yellow_heart:"https://github.githubassets.com/images/icons/emoji/unicode/1f49b.png?v8", + yellow_square:"https://github.githubassets.com/images/icons/emoji/unicode/1f7e8.png?v8", + yemen:"https://github.githubassets.com/images/icons/emoji/unicode/1f1fe-1f1ea.png?v8", + yen:"https://github.githubassets.com/images/icons/emoji/unicode/1f4b4.png?v8", + yin_yang:"https://github.githubassets.com/images/icons/emoji/unicode/262f.png?v8", + yo_yo:"https://github.githubassets.com/images/icons/emoji/unicode/1fa80.png?v8", + yum:"https://github.githubassets.com/images/icons/emoji/unicode/1f60b.png?v8", + zambia:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1f2.png?v8", + zany_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f92a.png?v8", + zap:"https://github.githubassets.com/images/icons/emoji/unicode/26a1.png?v8", + zebra:"https://github.githubassets.com/images/icons/emoji/unicode/1f993.png?v8", + zero:"https://github.githubassets.com/images/icons/emoji/unicode/0030-20e3.png?v8", + zimbabwe:"https://github.githubassets.com/images/icons/emoji/unicode/1f1ff-1f1fc.png?v8", + zipper_mouth_face:"https://github.githubassets.com/images/icons/emoji/unicode/1f910.png?v8", + zombie:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df.png?v8", + zombie_man:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df-2642.png?v8", + zombie_woman:"https://github.githubassets.com/images/icons/emoji/unicode/1f9df-2640.png?v8", + zzz:"https://github.githubassets.com/images/icons/emoji/unicode/1f4a4.png?v8", + }, + }) +} diff --git a/source/app/mocks/api/github/rest/rateLimit/get.mjs b/source/app/mocks/api/github/rest/rateLimit/get.mjs index 1364ab88..47b4f714 100644 --- a/source/app/mocks/api/github/rest/rateLimit/get.mjs +++ b/source/app/mocks/api/github/rest/rateLimit/get.mjs @@ -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}, + }, + }) +} diff --git a/source/app/mocks/api/github/rest/repos/getContributorsStats.mjs b/source/app/mocks/api/github/rest/repos/getContributorsStats.mjs index 50571f8a..062cb916 100644 --- a/source/app/mocks/api/github/rest/repos/getContributorsStats.mjs +++ b/source/app/mocks/api/github/rest/repos/getContributorsStats.mjs @@ -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, }, - ], - }) - } + }, + ], + }) +} diff --git a/source/app/mocks/api/github/rest/repos/getViews.mjs b/source/app/mocks/api/github/rest/repos/getViews.mjs index 4a3bb15a..1a94c1da 100644 --- a/source/app/mocks/api/github/rest/repos/getViews.mjs +++ b/source/app/mocks/api/github/rest/repos/getViews.mjs @@ -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}, + ], + }, + }) +} diff --git a/source/app/mocks/api/github/rest/repos/listCommits.mjs b/source/app/mocks/api/github/rest/repos/listCommits.mjs index e3d524bf..e996f3cb 100644 --- a/source/app/mocks/api/github/rest/repos/listCommits.mjs +++ b/source/app/mocks/api/github/rest/repos/listCommits.mjs @@ -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)}`, }, - })) : [], - }) - } + }, + })) + : [], + }) +} diff --git a/source/app/mocks/api/github/rest/repos/listContributors.mjs b/source/app/mocks/api/github/rest/repos/listContributors.mjs index 8ed003aa..1fde85bf 100644 --- a/source/app/mocks/api/github/rest/repos/listContributors.mjs +++ b/source/app/mocks/api/github/rest/repos/listContributors.mjs @@ -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), + })), + }) +} diff --git a/source/app/mocks/api/github/rest/request.mjs b/source/app/mocks/api/github/rest/request.mjs index 4c33291d..35b29237 100644 --- a/source/app/mocks/api/github/rest/request.mjs +++ b/source/app/mocks/api/github/rest/request.mjs @@ -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) +} diff --git a/source/app/mocks/api/github/rest/users/getByUsername.mjs b/source/app/mocks/api/github/rest/users/getByUsername.mjs index c346020d..2e0e7d45 100644 --- a/source/app/mocks/api/github/rest/users/getByUsername.mjs +++ b/source/app/mocks/api/github/rest/users/getByUsername.mjs @@ -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), + }, + }) +} diff --git a/source/app/mocks/index.mjs b/source/app/mocks/index.mjs index 28787d8b..a93088d8 100644 --- a/source/app/mocks/index.mjs +++ b/source/app/mocks/index.mjs @@ -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: "(?.*?)"/)?.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: "(?.*?)"/)?.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} +} diff --git a/source/app/web/instance.mjs b/source/app/web/instance.mjs index ed740550..d1a1aee5 100644 --- a/source/app/web/instance.mjs +++ b/source/app/web/instance.mjs @@ -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(/`(?[\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(/`(?[\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(/`(?[\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(/`(?[\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"))) +} diff --git a/source/app/web/statics/about/script.js b/source/app/web/statics/about/script.js index 72a145a7..04a46c5e 100644 --- a/source/app/web/statics/about/script.js +++ b/source/app/web/statics/about/script.js @@ -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, + }, + }) })() diff --git a/source/app/web/statics/app.js b/source/app/web/statics/app.js index f4ebb901..eac432b0 100644 --- a/source/app/web/statics/app.js +++ b/source/app/web/statics/app.js @@ -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 `![Metrics](${this.url})` - }, - //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)", }, - }) -})() \ No newline at end of file + }, + 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 `![Metrics](${this.url})` + }, + //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 + } + }, + }, + }) +})() diff --git a/source/app/web/statics/app.placeholder.js b/source/app/web/statics/app.placeholder.js index 9449f96c..bf2034dc 100644 --- a/source/app/web/statics/app.placeholder.js +++ b/source/app/web/statics/app.placeholder.js @@ -1,828 +1,1008 @@ -(function ({axios, faker, ejs} = {axios:globalThis.axios, faker:globalThis.faker, ejs:globalThis.ejs}) { +;(function({ axios, faker, ejs } = { axios: globalThis.axios, faker: globalThis.faker, ejs: globalThis.ejs }) { //Load assets - const cached = new Map() - async function load(url) { - if (!cached.has(url)) - cached.set(url, (await axios.get(url)).data) - return cached.get(url) - } + const cached = new Map() + async function load(url) { + if (!cached.has(url)) + cached.set(url, (await axios.get(url)).data) + return cached.get(url) + } //Distribution function - function distribution(length) { - let probability = 1 - const values = [] - for (let i = 0; i < length-1; i++) { - const value = Math.random()*probability - values.push(value) - probability -= value - } - values.push(probability) - return values.sort((a, b) => b - a) + function distribution(length) { + let probability = 1 + const values = [] + for (let i = 0; i < length - 1; i++) { + const value = Math.random() * probability + values.push(value) + probability -= value } + values.push(probability) + return values.sort((a, b) => b - a) + } //Placeholder function - globalThis.placeholder = async function (set) { - //Load templates informations - let {image, style, fonts, partials} = await load(`/.templates/${set.templates.selected}`) - await Promise.all(partials.map(async partial => await load(`/.templates/${set.templates.selected}/partials/${partial}.ejs`))) - //Trap includes - image = image.replace(/<%-\s*await include[(](`.*?[.]ejs`)[)]\s*%>/g, (m, g) => `<%- await $include(${g}) %>`) - //Faked data - const options = set.plugins.options - const data = { - //Template elements - style, fonts, errors:[], - partials:new Set([...(set.config.order||"").split(",").map(x => x.trim()).filter(x => partials.includes(x)), ...partials]), - //Plural helper - s(value, end = "") { - return value !== 1 ? {y:"ies", "":"s"}[end] : end - }, - //Formatter helper - f(n, {sign = false} = {}) { - 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(2).substr(0, 4).replace(/[.]0*$/, "")}${u}` - return `${(sign)&&(n > 0) ? "+" : ""}${n}` - }, - //Trap for includes - async $include(path) { - const partial = await load(`/.templates/${set.templates.selected}/${path}`) - return await ejs.render(partial, data, {async:true, rmWhitespace:true}) - }, - //Meta-data - meta:{version:set.version, author:"lowlighter"}, - //Animated - animated:false, - //Config - config:set.config, - //Base elements - base:set.plugins.enabled.base, - //Computed elements - computed: { - commits:faker.datatype.number(10000), - sponsorships:faker.datatype.number(10), - licenses:{favorite:[""], used:{MIT:1}}, - token:{scopes:[]}, - repositories: { - watchers:faker.datatype.number(1000), - stargazers:faker.datatype.number(10000), - issues_open:faker.datatype.number(1000), - issues_closed:faker.datatype.number(1000), - pr_open:faker.datatype.number(1000), - pr_closed:{totalCount:faker.datatype.number(100)}, - pr_merged:faker.datatype.number(1000), - forks:faker.datatype.number(1000), - releases:faker.datatype.number(1000), + globalThis.placeholder = async function(set) { + //Load templates informations + let { image, style, fonts, partials } = await load(`/.templates/${set.templates.selected}`) + await Promise.all(partials.map(async partial => await load(`/.templates/${set.templates.selected}/partials/${partial}.ejs`))) + //Trap includes + image = image.replace(/<%-\s*await include[(](`.*?[.]ejs`)[)]\s*%>/g, (m, g) => `<%- await $include(${g}) %>`) + //Faked data + const options = set.plugins.options + const data = { + //Template elements + style, + fonts, + errors: [], + partials: new Set([...(set.config.order || "").split(",").map(x => x.trim()).filter(x => partials.includes(x)), ...partials]), + //Plural helper + s(value, end = "") { + return value !== 1 ? { y: "ies", "": "s" }[end] : end + }, + //Formatter helper + f(n, { sign = false } = {}) { + 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(2).substr(0, 4).replace(/[.]0*$/, "")}${u}` + } + return `${(sign) && (n > 0) ? "+" : ""}${n}` + }, + //Trap for includes + async $include(path) { + const partial = await load(`/.templates/${set.templates.selected}/${path}`) + return await ejs.render(partial, data, { async: true, rmWhitespace: true }) + }, + //Meta-data + meta: { version: set.version, author: "lowlighter" }, + //Animated + animated: false, + //Config + config: set.config, + //Base elements + base: set.plugins.enabled.base, + //Computed elements + computed: { + commits: faker.datatype.number(10000), + sponsorships: faker.datatype.number(10), + licenses: { favorite: [""], used: { MIT: 1 } }, + token: { scopes: [] }, + repositories: { + watchers: faker.datatype.number(1000), + stargazers: faker.datatype.number(10000), + issues_open: faker.datatype.number(1000), + issues_closed: faker.datatype.number(1000), + pr_open: faker.datatype.number(1000), + pr_closed: { totalCount: faker.datatype.number(100) }, + pr_merged: faker.datatype.number(1000), + forks: faker.datatype.number(1000), + releases: faker.datatype.number(1000), + }, + diskUsage: `${faker.datatype.float({ min: 1, max: 999 }).toFixed(1)}MB`, + registration: `${faker.datatype.number({ min: 2, max: 10 })} years ago`, + cakeday: false, + calendar: new Array(14).fill(null).map(_ => ({ color: faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"]) })), + avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + }, + //User data + account: "user", + user: { + databaseId: faker.datatype.number(10000000), + name: "(placeholder)", + login: set.user || "metrics", + createdAt: `${faker.date.past(10)}`, + avatarUrl: set.avatar, + websiteUrl: options["pagespeed.url"] || "(attached website)", + isHireable: false, + twitterUsername: options["tweets.user"] || "(attached Twitter account)", + 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: [] } }, + 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) }, + }, + //Plugins + plugins: { + //Tweets + ...(set.plugins.enabled.tweets + ? ({ + tweets: { + username: options["tweets.user"] || "(attached Twitter account)", + profile: { + profile_image_url: faker.image.people(), + name: "", + verified: false, + id: faker.datatype.number(1000000).toString(), + username: options["tweets.user"] || "(attached Twitter account)", + profile_image: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", }, - diskUsage:`${faker.datatype.float({min:1, max:999}).toFixed(1)}MB`, - registration:`${faker.datatype.number({min:2, max:10})} years ago`, - cakeday:false, - calendar:new Array(14).fill(null).map(_ => ({color:faker.random.arrayElement(["#ebedf0", "#9be9a8", "#40c463", "#30a14e", "#216e39"])})), - avatar:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" + list: [ + { + 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 ', + mentions: ["lowlighter"], + }, + ...new Array(Number(options["tweets.limit"]) - 1).fill(null).map(_ => ({ + id: faker.datatype.number(100000000000000).toString(), + created_at: faker.date.recent(), + text: faker.lorem.paragraph(), + mentions: [], + })), + ], }, - //User data - account:"user", - user:{ - databaseId:faker.datatype.number(10000000), - name:"(placeholder)", - login:set.user||"metrics", - createdAt:`${faker.date.past(10)}`, - avatarUrl:set.avatar, - websiteUrl:options["pagespeed.url"]||"(attached website)", - isHireable:false, - twitterUsername:options["tweets.user"]||"(attached Twitter account)", - 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), + }) + : null), + //Lines + ...(set.plugins.enabled.lines + ? ({ + lines: { + added: `${faker.datatype.number(100)}.${faker.datatype.number(9)}k`, + deleted: `${faker.datatype.number(100)}.${faker.datatype.number(9)}k`, + }, + }) + : null), + //Traffic + ...(set.plugins.enabled.traffic + ? ({ + traffic: { + views: { + count: `${faker.datatype.number({ min: 10, max: 100 })}.${faker.datatype.number(9)}k`, + uniques: `${faker.datatype.number(10)}.${faker.datatype.number(9)}k`, }, - calendar:{contributionCalendar:{weeks:[]}}, - 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)} }, - //Plugins - plugins:{ - //Tweets - ...(set.plugins.enabled.tweets ? ({ - tweets:{ - username:options["tweets.user"]||"(attached Twitter account)", - profile:{ - profile_image_url:faker.image.people(), - name:"", - verified:false, - id:faker.datatype.number(1000000).toString(), - username:options["tweets.user"]||"(attached Twitter account)", - profile_image:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", - }, - list:[ - { - 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 ', - mentions: ["lowlighter"] - }, - ...new Array(Number(options["tweets.limit"])-1).fill(null).map(_ => ({ - id:faker.datatype.number(100000000000000).toString(), - created_at:faker.date.recent(), - text:faker.lorem.paragraph(), - mentions:[] - })), - ] - } - }) : null), - //Lines - ...(set.plugins.enabled.lines ? ({ - lines:{ - added:`${faker.datatype.number(100)}.${faker.datatype.number(9)}k`, - deleted:`${faker.datatype.number(100)}.${faker.datatype.number(9)}k`, - } - }) : null), - //Traffic - ...(set.plugins.enabled.traffic ? ({ - traffic:{ - views:{ - count:`${faker.datatype.number({min:10, max:100})}.${faker.datatype.number(9)}k`, - uniques:`${faker.datatype.number(10)}.${faker.datatype.number(9)}k`, - } - } - }) : null), - //Follow-up - ...(set.plugins.enabled.followup ? ({ - followup:{ - sections:options["followup.sections"].split(",").map(x => x.trim()).filter(x => ["user", "repositories"].includes(x)), - issues:{get count() { return this.open + this.closed }, open:faker.datatype.number(1000), closed:faker.datatype.number(1000)}, - pr:{get count() { return this.open + this.merged }, open:faker.datatype.number(1000), closed:faker.datatype.number(1000), merged:faker.datatype.number(1000)}, - user:{ - issues:{get count() { return this.open + this.closed }, open:faker.datatype.number(1000), closed:faker.datatype.number(1000)}, - pr:{get count() { return this.open + this.merged }, open:faker.datatype.number(1000), closed:faker.datatype.number(1000), merged:faker.datatype.number(1000)}, - } - } - }) : null), - //Notable - ...(set.plugins.enabled.notable ? ({ - notable:{ - contributions:new Array(2+faker.datatype.number(2)).fill(null).map(_ => ({name:faker.lorem.slug(), avatar:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg=="})), - } - }) : null), - //Gists - ...(set.plugins.enabled.gists ? ({ - gists:{ - totalCount:faker.datatype.number(100), - stargazers:faker.datatype.number(1000), - forks:faker.datatype.number(100), - files:faker.datatype.number(100), - comments:faker.datatype.number(1000) - } - }) : null), - //Reactions - ...(set.plugins.enabled.reactions ? ({ - reactions:{ - list:{ - HEART:{value:faker.datatype.number(100), get percentage() { return this.score }, score:faker.datatype.number(100)/100}, - THUMBS_UP:{value:faker.datatype.number(100), get percentage() { return this.score }, score:faker.datatype.number(100)/100}, - THUMBS_DOWN:{value:faker.datatype.number(100), get percentage() { return this.score }, score:faker.datatype.number(100)/100}, - LAUGH:{value:faker.datatype.number(100), get percentage() { return this.score }, score:faker.datatype.number(100)/100}, - CONFUSED:{value:faker.datatype.number(100), get percentage() { return this.score }, score:faker.datatype.number(100)/100}, - EYES:{value:faker.datatype.number(100), get percentage() { return this.score }, score:faker.datatype.number(100)/100}, - ROCKET:{value:faker.datatype.number(100), get percentage() { return this.score }, score:faker.datatype.number(100)/100}, - HOORAY:{value:faker.datatype.number(100), get percentage() { return this.score }, score:faker.datatype.number(100)/100}, - }, - comments:options["reactions.limit"], - details:options["reactions.details"], - days:options["reactions.days"] - } - }) : null), - //Achievements - ...(set.plugins.enabled.achievements ? ({ - achievements:{ - list:new Array(8).fill(null).map(_ => ({ - title:faker.lorem.word(), - unlock:null, - text:faker.lorem.sentence(), - icon:``, - rank:faker.random.arrayElement(["A", "B", "C", "X", "$"]), - progress:faker.datatype.number(100)/100, - value:faker.datatype.number(1000), - })) - .filter(({rank}) => options["achievements.secrets"] ? true : rank !== "$") - .filter(({rank}) => ({S:5, A:4, B:3, C:2, $:1, X:0}[rank] >= {S:5, A:4, B:3, C:2, $:1, X:0}[options["achievements.threshold"]])) - .sort((a, b) => ({S:5, A:4, B:3, C:2, $:1, X:0}[b.rank]+b.progress*0.99) - ({S:5, A:4, B:3, C:2, $:1, X:0}[a.rank]+a.progress*0.99)) - .slice(0, options["achievements.limit"] || Infinity) - , - } - }) : null), - //Introduction - ...(set.plugins.enabled.introduction ? ({ - introduction:{ - mode:"user", - title:options["introduction.title"], - text:faker.lorem.sentences(), - } - }) : null), - //Languages - ...(set.plugins.enabled.languages ? ({ - languages:{ - details:options["languages.details"].split(",").map(x => x.trim()).filter(x => x), - get colors() { return Object.fromEntries(Object.entries(this.favorites).map(([key, {color}]) => [key, color])) }, - total:faker.datatype.number(10000), - get stats() { return Object.fromEntries(Object.entries(this.favorites).map(([key, {value}]) => [key, value])) }, - favorites:distribution(7).map((value, index, array) => ({name:faker.lorem.word(), color:faker.internet.color(), value, size:faker.datatype.number(1000000), x:array.slice(0, index).reduce((a, b) => a + b, 0)})) - } - }) : null), - //RSS - ...(set.plugins.enabled.rss ? ({ - rss:{ - source:faker.lorem.words(), - description:faker.lorem.paragraph(), - link:options["rss.source"], - feed:new Array(Number(options["rss.limit"])).fill(null).map(_ => ({ - title:faker.lorem.sentence(), - date:faker.date.recent() - })), - } - }) : null), - //Stock price - ...(set.plugins.enabled.stock ? ({ - stock:{ - chart:"(stock chart is not displayed in placeholder)", - currency:"USD", - price:faker.datatype.number(10000)/100, - previous:faker.datatype.number(10000)/100, - get delta() { return this.price-this.previous }, - symbol:options["stock.symbol"], - company:faker.company.companyName(), - interval:options["stock.interval"], - duration:options["stock.duration"], - } - }) : null), - //Habits - ...(set.plugins.enabled.habits ? ({ - habits:{ - facts:options["habits.facts"], - charts:options["habits.charts"], - commits:{ - get hour() { return Object.keys(this.hours).filter(key => /^\d+$/.test(key)).map(key => [key, this.hours[key]]).sort((a, b) => b[1] - a[1]).shift()?.[0] }, - hours:{ - [faker.datatype.number(24)]:faker.datatype.number(10), - [faker.datatype.number(24)]:faker.datatype.number(10), - [faker.datatype.number(24)]:faker.datatype.number(10), - [faker.datatype.number(24)]:faker.datatype.number(10), - [faker.datatype.number(24)]:faker.datatype.number(10), - [faker.datatype.number(24)]:faker.datatype.number(10), - [faker.datatype.number(24)]:faker.datatype.number(10), - get max() { return Object.keys(this).filter(key => /^\d+$/.test(key)).map(key => [key, this[key]]).sort((a, b) => b[1] - a[1]).shift()?.[1] } - }, - get day() { return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][Object.keys(this.days).filter(key => /^\d+$/.test(key)).map(key => [key, this.days[key]]).sort((a, b) => b[1] - a[1]).shift()?.[0]] }, - days:{ - "0":faker.datatype.number(10), - "1":faker.datatype.number(10), - "2":faker.datatype.number(10), - "3":faker.datatype.number(10), - "4":faker.datatype.number(10), - "5":faker.datatype.number(10), - "6":faker.datatype.number(10), - get max() { return Object.keys(this).filter(key => /^\d+$/.test(key)).map(key => [key, this[key]]).sort((a, b) => b[1] - a[1]).shift()?.[1] } - }, - }, - indents:{style:"spaces", spaces:1, tabs:0}, - linguist:{ - available:true, - get ordered() { return Object.entries(this.languages) }, - get languages() { return Object.fromEntries(distribution(4).map(value => [faker.lorem.word(), value])) }, - } - } - }) : null), - //People - ...(set.plugins.enabled.people ? ({ - get people() { - const types = options["people.types"].split(",").map(x => x.trim()) - .map(x => ({followed:"following", sponsors:"sponsorshipsAsMaintainer", sponsored:"sponsorshipsAsSponsor", sponsoring:"sponsorshipsAsSponsor"})[x] ?? x) - .filter(x => ["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor"].includes(x)) - return { - types, - size:options["people.size"], - ...(Object.fromEntries(types.map(type => [ - type, - new Array(Number(options["people.limit"])).fill(null).map(_ => ({ - login:faker.internet.userName(), - avatar:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", - })) - ]))), - thanks:options["people.thanks"].split(",").map(x => x.trim()).map(login => ({ - login, - avatar:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", - })) - } - } - }) : null), - //Music - ...(set.plugins.enabled.music ? ({ - music:{ - provider:"(music provider)", - mode:"Suggested tracks", - tracks:new Array(Number(options["music.limit"])).fill(null).map(_ => ({ - name:faker.random.words(5), - artist:faker.random.words(), - artwork:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", - })) - } - }) : null), - //Nightscout - ...(set.plugins.enabled.nightscout ? ({ - nightscout:{ - url: options["nightscout.url"] != null && options["nightscout.url"] != "https://example.herokuapp.com" ? options["nightscout.url"]: "https://testapp.herokuapp.com/", - data:new Array(12).fill(null).map(_ => ({ - timeUTCHumanReadable:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`, - color:faker.random.arrayElement(["#9be9a8", "#40c463", "#30a14e", "#216e39"]), - 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"]), - alert:faker.random.arrayElement(["Normal", "Urgent High", "Urgent Low", "High", "Low"]), - arrowHumanReadable:faker.random.arrayElement(["↑↑", "↑", "↗", "→", "↘", "↓", "↓↓"]), + }) + : null), + //Follow-up + ...(set.plugins.enabled.followup + ? ({ + followup: { + sections: options["followup.sections"].split(",").map(x => x.trim()).filter(x => ["user", "repositories"].includes(x)), + issues: { + get count() { + return this.open + this.closed + }, + open: faker.datatype.number(1000), + closed: faker.datatype.number(1000), + }, + pr: { + get count() { + return this.open + this.merged + }, + open: faker.datatype.number(1000), + closed: faker.datatype.number(1000), + merged: faker.datatype.number(1000), + }, + user: { + issues: { + get count() { + return this.open + this.closed + }, + open: faker.datatype.number(1000), + closed: faker.datatype.number(1000), + }, + pr: { + get count() { + return this.open + this.merged + }, + open: faker.datatype.number(1000), + closed: faker.datatype.number(1000), + merged: faker.datatype.number(1000), + }, + }, + }, + }) + : null), + //Notable + ...(set.plugins.enabled.notable + ? ({ + notable: { + contributions: new Array(2 + faker.datatype.number(2)).fill(null).map(_ => ({ name: faker.lorem.slug(), avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" })), + }, + }) + : null), + //Gists + ...(set.plugins.enabled.gists + ? ({ + gists: { + totalCount: faker.datatype.number(100), + stargazers: faker.datatype.number(1000), + forks: faker.datatype.number(100), + files: faker.datatype.number(100), + comments: faker.datatype.number(1000), + }, + }) + : null), + //Reactions + ...(set.plugins.enabled.reactions + ? ({ + reactions: { + list: { + HEART: { + value: faker.datatype.number(100), + get percentage() { + return this.score + }, + score: faker.datatype.number(100) / 100, + }, + THUMBS_UP: { + value: faker.datatype.number(100), + get percentage() { + return this.score + }, + score: faker.datatype.number(100) / 100, + }, + THUMBS_DOWN: { + value: faker.datatype.number(100), + get percentage() { + return this.score + }, + score: faker.datatype.number(100) / 100, + }, + LAUGH: { + value: faker.datatype.number(100), + get percentage() { + return this.score + }, + score: faker.datatype.number(100) / 100, + }, + CONFUSED: { + value: faker.datatype.number(100), + get percentage() { + return this.score + }, + score: faker.datatype.number(100) / 100, + }, + EYES: { + value: faker.datatype.number(100), + get percentage() { + return this.score + }, + score: faker.datatype.number(100) / 100, + }, + ROCKET: { + value: faker.datatype.number(100), + get percentage() { + return this.score + }, + score: faker.datatype.number(100) / 100, + }, + HOORAY: { + value: faker.datatype.number(100), + get percentage() { + return this.score + }, + score: faker.datatype.number(100) / 100, + }, + }, + comments: options["reactions.limit"], + details: options["reactions.details"], + days: options["reactions.days"], + }, + }) + : null), + //Achievements + ...(set.plugins.enabled.achievements + ? ({ + achievements: { + list: new Array(8).fill(null).map(_ => ({ + title: faker.lorem.word(), + unlock: null, + text: faker.lorem.sentence(), + icon: + ``, + rank: faker.random.arrayElement(["A", "B", "C", "X", "$"]), + progress: faker.datatype.number(100) / 100, + value: faker.datatype.number(1000), + })) + .filter(({ rank }) => options["achievements.secrets"] ? true : rank !== "$") + .filter(({ rank }) => ({ S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[rank] >= { S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[options["achievements.threshold"]])) + .sort((a, b) => ({ S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[b.rank] + b.progress * 0.99) - ({ S: 5, A: 4, B: 3, C: 2, $: 1, X: 0 }[a.rank] + a.progress * 0.99)) + .slice(0, options["achievements.limit"] || Infinity), + }, + }) + : null), + //Introduction + ...(set.plugins.enabled.introduction + ? ({ + introduction: { + mode: "user", + title: options["introduction.title"], + text: faker.lorem.sentences(), + }, + }) + : null), + //Languages + ...(set.plugins.enabled.languages + ? ({ + languages: { + details: options["languages.details"].split(",").map(x => x.trim()).filter(x => x), + get colors() { + return Object.fromEntries(Object.entries(this.favorites).map(([key, { color }]) => [key, color])) + }, + total: faker.datatype.number(10000), + get stats() { + return Object.fromEntries(Object.entries(this.favorites).map(([key, { value }]) => [key, value])) + }, + favorites: distribution(7).map((value, index, array) => ({ name: faker.lorem.word(), color: faker.internet.color(), value, size: faker.datatype.number(1000000), x: array.slice(0, index).reduce((a, b) => a + b, 0) })), + }, + }) + : null), + //RSS + ...(set.plugins.enabled.rss + ? ({ + rss: { + source: faker.lorem.words(), + description: faker.lorem.paragraph(), + link: options["rss.source"], + feed: new Array(Number(options["rss.limit"])).fill(null).map(_ => ({ + title: faker.lorem.sentence(), + date: faker.date.recent(), + })), + }, + }) + : null), + //Stock price + ...(set.plugins.enabled.stock + ? ({ + stock: { + chart: "(stock chart is not displayed in placeholder)", + currency: "USD", + price: faker.datatype.number(10000) / 100, + previous: faker.datatype.number(10000) / 100, + get delta() { + return this.price - this.previous + }, + symbol: options["stock.symbol"], + company: faker.company.companyName(), + interval: options["stock.interval"], + duration: options["stock.duration"], + }, + }) + : null), + //Habits + ...(set.plugins.enabled.habits + ? ({ + habits: { + facts: options["habits.facts"], + charts: options["habits.charts"], + commits: { + get hour() { + return Object.keys(this.hours).filter(key => /^\d+$/.test(key)).map(key => [key, this.hours[key]]).sort((a, b) => b[1] - a[1]).shift()?.[0] + }, + hours: { + [faker.datatype.number(24)]: faker.datatype.number(10), + [faker.datatype.number(24)]: faker.datatype.number(10), + [faker.datatype.number(24)]: faker.datatype.number(10), + [faker.datatype.number(24)]: faker.datatype.number(10), + [faker.datatype.number(24)]: faker.datatype.number(10), + [faker.datatype.number(24)]: faker.datatype.number(10), + [faker.datatype.number(24)]: faker.datatype.number(10), + get max() { + return Object.keys(this).filter(key => /^\d+$/.test(key)).map(key => [key, this[key]]).sort((a, b) => b[1] - a[1]).shift()?.[1] + }, + }, + get day() { + return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][Object.keys(this.days).filter(key => /^\d+$/.test(key)).map(key => [key, this.days[key]]).sort((a, b) => b[1] - a[1]).shift()?.[0]] + }, + days: { + "0": faker.datatype.number(10), + "1": faker.datatype.number(10), + "2": faker.datatype.number(10), + "3": faker.datatype.number(10), + "4": faker.datatype.number(10), + "5": faker.datatype.number(10), + "6": faker.datatype.number(10), + get max() { + return Object.keys(this).filter(key => /^\d+$/.test(key)).map(key => [key, this[key]]).sort((a, b) => b[1] - a[1]).shift()?.[1] + }, + }, + }, + indents: { style: "spaces", spaces: 1, tabs: 0 }, + linguist: { + available: true, + get ordered() { + return Object.entries(this.languages) + }, + get languages() { + return Object.fromEntries(distribution(4).map(value => [faker.lorem.word(), value])) + }, + }, + }, + }) + : null), + //People + ...(set.plugins.enabled.people + ? ({ + get people() { + const types = options["people.types"].split(",").map(x => x.trim()) + .map(x => ({ followed: "following", sponsors: "sponsorshipsAsMaintainer", sponsored: "sponsorshipsAsSponsor", sponsoring: "sponsorshipsAsSponsor" })[x] ?? x) + .filter(x => ["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor"].includes(x)) + return { + types, + size: options["people.size"], + ...(Object.fromEntries(types.map(type => [ + type, + new Array(Number(options["people.limit"])).fill(null).map(_ => ({ + login: faker.internet.userName(), + avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", })), - } - }) : null), - //Pagespeed - ...(set.plugins.enabled.pagespeed ? ({ - pagespeed:{ - url:options["pagespeed.url"]||"(attached website url)", - detailed:options["pagespeed.detailed"]||false, - scores: [ - {score:faker.datatype.float({max:1}), title:"Performance"}, - {score:faker.datatype.float({max:1}), title:"Accessibility"}, - {score:faker.datatype.float({max:1}), title:"Best Practices"}, - {score:faker.datatype.float({max:1}), title:"SEO"} - ], - metrics:{ - 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() - }, - screenshot:options["pagespeed.screenshot"] ? "data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" : null - } - }) : null), - //Projects - ...(set.plugins.enabled.projects ? ({ - projects:{ - totalCount:options["projects.limit"]+faker.datatype.number(10), - descriptions:options["projects.descriptions"], - list:new Array(Number(options["projects.limit"])).fill(null).map(_ => ({ - name:faker.lorem.sentence(), - description:faker.lorem.paragraph(), - updated:`${2+faker.datatype.number(8)} days ago`, - progress:{enabled:true, todo:faker.datatype.number(50), doing:faker.datatype.number(50), done:faker.datatype.number(50), get total() { return this.todo + this.doing + this.done } } - })) - } - }) : null), - //Posts - ...(set.plugins.enabled.posts ? ({ - posts:{ - source:options["posts.source"], - descriptions:options["posts.descriptions"], - covers:options["posts.covers"], - list:new Array(Number(options["posts.limit"])).fill(null).map(_ => ({ - title:faker.lorem.sentence(), - description:faker.lorem.paragraph(), - date:faker.date.recent(), - image:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", - })) - } - }) : null), - //Topics - ...(set.plugins.enabled.topics ? ({ - topics:{ - mode:options["topics.mode"], - list:new Array(Number(options["topics.limit"])||20).fill(null).map(_ => ({ - name:faker.lorem.words(2), - description:faker.lorem.sentence(), - icon:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" - })) - } - }) : null), - //Stars - ...(set.plugins.enabled.stars ? ({ - stars:{ - repositories: [ - { - starredAt:faker.date.recent(), - 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"} - }, - starred:"1 day ago" - }, - ...new Array(Number(options["stars.limit"])-1).fill(null).map((_, i) => ({ - starredAt:faker.date.recent(), - node: { - description:faker.lorem.sentence(), - forkCount:faker.datatype.number(100), - isFork:faker.datatype.boolean(), - issues:{ - totalCount:faker.datatype.number(100), - }, - nameWithOwner:`${faker.random.word()}/${faker.random.word()}`, - openGraphImageUrl:faker.internet.url(), - pullRequests:{ - totalCount:faker.datatype.number(100), - }, - stargazerCount:faker.datatype.number(10000), - licenseInfo:{nickname:null, name:"License"}, - primaryLanguage:{color:faker.internet.color(), name:faker.lorem.word()} - }, - starred:`${i+2} days ago` - })), - ] - } - }) : null), - //Stars - ...(set.plugins.enabled.stargazers ? ({ - get stargazers() { - const dates = [] - let total = faker.datatype.number(1000) - const result = { - total:{ - dates:{}, - get max() { return Math.max(...dates.map(date => this.dates[date])) }, - get min() { return Math.min(...dates.map(date => this.dates[date])) }, - }, - increments:{ - dates:{}, - get max() { return Math.max(...dates.map(date => this.dates[date])) }, - get min() { return Math.min(...dates.map(date => this.dates[date])) }, - }, - months:["", "Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."] - } - for (let d = -14; d <= 0; d++) { - const date = new Date(Date.now()-d*24*60*60*1000).toISOString().substring(0, 10) - dates.push(date) - result.total.dates[date] = (total += (result.increments.dates[date] = faker.datatype.number(100))) - } - return result - } - }) : null), - //Wakatime - ...(set.plugins.enabled.wakatime ? ({ - get wakatime() { - const stats = (array) => { - const elements = [] - let results = new Array(4+faker.datatype.number(2)).fill(null).map(_ => ({ - name:array ? faker.random.arrayElement(array) : faker.random.words(2).replace(/ /g, "-").toLocaleLowerCase(), - percent:0, total_seconds:faker.datatype.number(1000000), - })) - let percents = 100 - for (const result of results) { - result.percent = 1+faker.datatype.number(percents-1) - percents -= result.percent - result.percent /= 100 - } - results.filter(({name}) => elements.includes(name) ? false : (elements.push(name), true)) - return results.sort((a, b) => b.percent - a.percent) - } - return { - sections:options["wakatime.sections"].split(",").map(x => x.trim()).filter(x => x), - days:Number(options["wakatime.days"])||7, - time:{total:faker.datatype.number(100000), daily:faker.datatype.number(24)}, - editors:stats(["VS Code", "Chrome", "IntelliJ", "PhpStorm", "WebStorm", "Android Studio", "Visual Studio", "Sublime Text", "PyCharm", "Vim", "Atom", "Xcode"]), - languages:stats(["JavaScript", "TypeScript", "PHP", "Java", "Python", "Vue.js", "HTML", "C#", "JSON", "Dart", "SCSS", "Kotlin", "JSX", "Go", "Ruby", "YAML"]), - projects:stats(), - os:stats(["Mac", "Windows", "Linux"]), - } - } - }) : null), - //Anilist - ...(set.plugins.enabled.anilist ? ({ - anilist:{ - user:{ - stats:{ - 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()})), - }, - }, - genres:new Array(4).fill(null).map(_ => ({genre:faker.lorem.word()})), - }, - get lists() { - const media = (type) => ({ - name:faker.lorem.words(), - type, - status:faker.random.arrayElement(["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]), - release:faker.date.past(20).getFullYear(), - genres:new Array(6).fill(null).map(_ => faker.lorem.word()), - progress:faker.datatype.number(100), - description:faker.lorem.paragraphs(), - scores:{user:faker.datatype.number(100), community:faker.datatype.number(100)}, - released:100+faker.datatype.number(1000), - artwork:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", - }) - const sections = options["anilist.sections"].split(",").map(x => x.trim()).filter(x => x) - const medias = options["anilist.medias"].split(",").map(x => x.trim()).filter(x => x) - return { - ...(medias.includes("anime") ? {anime:{ - ...(sections.includes("watching") ? {watching:new Array(Number(options["anilist.limit"])||4).fill(null).map(_ => media("ANIME"))} : {}), - ...(sections.includes("favorites") ? {favorites:new Array(Number(options["anilist.limit"])||4).fill(null).map(_ => media("ANIME"))} : {}), - }} : {}), - ...(medias.includes("manga") ? {manga:{ - ...(sections.includes("reading") ? {reading:new Array(Number(options["anilist.limit"])||4).fill(null).map(_ => media("MANGA"))} : {}), - ...(sections.includes("favorites") ? {favorites:new Array(Number(options["anilist.limit"])||4).fill(null).map(_ => media("MANGA"))} : {}), - }} : {}), - } - }, - characters:new Array(11).fill(null).map(_ => ({ - name:faker.name.findName(), - artwork:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", - })), - sections:options["anilist.sections"].split(",").map(x => x.trim()).filter(x => x) - } - }) : null), - //Activity - ...(set.plugins.enabled.activity ? ({ - activity:{ - timestamps:options["activity.timestamps"], - events:new Array(Number(options["activity.limit"])).fill(null).map(_ => [ - { - type:"push", - repo:`${faker.random.word()}/${faker.random.word()}`, - size:1, - branch:"master", - commits: [ { sha:faker.git.shortSha(), message:faker.lorem.sentence()} ], - timestamp:faker.date.recent(), - }, - { - type:"comment", - on:"commit", - repo:`${faker.random.word()}/${faker.random.word()}`, - content:faker.lorem.paragraph(), - user:set.user, - mobile:null, - number:faker.git.shortSha(), - title:"", - timestamp:faker.date.recent(), - }, - { - type:"comment", - on:"pr", - repo:`${faker.random.word()}/${faker.random.word()}`, - content:faker.lorem.sentence(), - user:set.user, - mobile:null, - number:faker.datatype.number(100), - title:faker.lorem.sentence(), - timestamp:faker.date.recent(), - }, - { - type:"comment", - on:"issue", - repo:`${faker.random.word()}/${faker.random.word()}`, - content:faker.lorem.sentence(), - user:set.user, - mobile:null, - number:faker.datatype.number(100), - title:faker.lorem.sentence(), - timestamp:faker.date.recent(), - }, - { - type:"issue", - repo:`${faker.random.word()}/${faker.random.word()}`, - action:faker.random.arrayElement(["opened", "closed", "reopened"]), - user:set.user, - number:faker.datatype.number(100), - title:faker.lorem.sentence(), - timestamp:faker.date.recent(), - }, - { - type:"pr", - repo:`${faker.random.word()}/${faker.random.word()}`, - action:faker.random.arrayElement(["opened", "closed"]), - user:set.user, - number:faker.datatype.number(100), - title:faker.lorem.sentence(), - lines:{added:faker.datatype.number(1000), deleted:faker.datatype.number(1000)}, files:{changed:faker.datatype.number(10)}, - timestamp:faker.date.recent(), - }, - { - type:"wiki", - repo:`${faker.random.word()}/${faker.random.word()}`, - pages:[faker.lorem.sentence(), faker.lorem.sentence()], - timestamp:faker.date.recent(), - }, - { - type:"fork", - repo:`${faker.random.word()}/${faker.random.word()}`, - timestamp:faker.date.recent(), - }, - { - type:"review", - repo:`${faker.random.word()}/${faker.random.word()}`, - user:set.user, - number:faker.datatype.number(100), - title:faker.lorem.sentence(), - timestamp:faker.date.recent(), - }, - { - type:"release", - repo:`${faker.random.word()}/${faker.random.word()}`, - action:"published", - name:faker.random.words(4), - draft:faker.datatype.boolean(), - prerelease:faker.datatype.boolean(), - timestamp:faker.date.recent(), - }, - { - type:"ref/create", - repo:`${faker.random.word()}/${faker.random.word()}`, - ref:{name:faker.lorem.slug(), type:faker.random.arrayElement(["tag", "branch"])}, - timestamp:faker.date.recent(), - }, - { - type:"ref/delete", - repo:`${faker.random.word()}/${faker.random.word()}`, - ref:{name:faker.lorem.slug(), type:faker.random.arrayElement(["tag", "branch"])}, - timestamp:faker.date.recent(), - }, - { - type:"member", - repo:`${faker.random.word()}/${faker.random.word()}`, - user:set.user, - timestamp:faker.date.recent(), - }, - { - type:"public", - repo:`${faker.random.word()}/${faker.random.word()}`, - timestamp:faker.date.recent(), - }, - { - type:"star", - repo:`${faker.random.word()}/${faker.random.word()}`, - action:"started", - timestamp:faker.date.recent(), - }, - ][Math.floor(Math.random()*15)]) - } - }) : null), - //Isocalendar - ...(set.plugins.enabled.isocalendar ? ({ - isocalendar:{ - streak:{max:30+faker.datatype.number(20), current:faker.datatype.number(30)}, - max:10+faker.datatype.number(40), - average:faker.datatype.float(10), - svg:"(isometric calendar is not displayed in placeholder)", - duration:options["isocalendar.duration"] - } - }) : null), - //Stackoverflow - ...(set.plugins.enabled.stackoverflow ? ({ - stackoverflow:{ - sections:options["stackoverflow.sections"].split(",").map(x => x.trim()).filter(x => x), - lines:options["stackoverflow.lines"], - user:{ - reputation:faker.datatype.number(100000), - badges:faker.datatype.number(1000), - questions:faker.datatype.number(1000), - answers:faker.datatype.number(1000), - comments:faker.datatype.number(1000), - views:faker.datatype.number(1000), - }, - "answers-top":new Array(options["stackoverflow.limit"]).fill(null).map(_ => ({ - type:"answer", - body:faker.lorem.paragraphs(), - score:faker.datatype.number(1000), - upvotes:faker.datatype.number(1000), - downvotes:faker.datatype.number(1000), - accepted:faker.datatype.boolean(), - comments:faker.datatype.number(1000), - author:set.user, - created:"01/01/1970", - link:null, - id:faker.datatype.number(100000), - question_id:faker.datatype.number(100000), - question:{ - title:faker.lorem.sentence(), - tags:[faker.lorem.slug(), faker.lorem.slug()], - } - })), - get ["answers-recent"]() { - return this["answers-top"] - }, - "questions-top":new Array(options["stackoverflow.limit"]).fill(null).map(_ => ({ - type:"question", - title:faker.lorem.sentence(), - body:faker.lorem.paragraphs(), - score:faker.datatype.number(1000), - upvotes:faker.datatype.number(1000), - downvotes:faker.datatype.number(1000), - favorites:faker.datatype.number(1000), - tags:[faker.lorem.slug(), faker.lorem.slug()], - answered:faker.datatype.boolean(), - answers:faker.datatype.number(1000), - comments:faker.datatype.number(1000), - views:faker.datatype.number(1000), - author:set.user, - created:"01/01/1970", - link:null, - id:faker.datatype.number(100000), - accepted_answer_id:faker.datatype.number(100000), - answer:null, - })), - get ["questions-recent"]() { - return this["questions-top"] - }, - } - }) : null), + ]))), + thanks: options["people.thanks"].split(",").map(x => x.trim()).map(login => ({ + login, + avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + })), + } }, - } - //Formatters - data.f.bytes = function (n) { - for (const {u, v} of [{u:"E", v:10**18}, {u:"P", v:10**15}, {u:"T", v:10**12}, {u:"G", v:10**9}, {u:"M", v:10**6}, {u:"k", v:10**3}]) - if (n/v >= 1) - return `${(n/v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B` - return `${n} byte${n > 1 ? "s" : ""}` - } - data.f.percentage = function (n, {rescale = true} = {}) { - return `${(n*(rescale ? 100 : 1)).toFixed(2) - .replace(/[.]([1-9]*)(0+)$/, (m, a, b) => `.${a}`) - .replace(/[.]$/, "")}%` - } - data.f.ellipsis = function (text, {length = 20} = {}) { - text = `${text}` - if (text.length < length) - return text - return `${text.substring(0, length)}…` - } - data.f.date = function (string, options) { - return new Intl.DateTimeFormat("en-GB", options).format(new Date(string)) - } - //Render - return await ejs.render(image, data, {async:true, rmWhitespace:true}) + }) + : null), + //Music + ...(set.plugins.enabled.music + ? ({ + music: { + provider: "(music provider)", + mode: "Suggested tracks", + tracks: new Array(Number(options["music.limit"])).fill(null).map(_ => ({ + name: faker.random.words(5), + artist: faker.random.words(), + artwork: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + })), + }, + }) + : null), + //Nightscout + ...(set.plugins.enabled.nightscout + ? ({ + nightscout: { + url: options["nightscout.url"] != null && options["nightscout.url"] != "https://example.herokuapp.com" ? options["nightscout.url"] : "https://testapp.herokuapp.com/", + data: new Array(12).fill(null).map(_ => ({ + timeUTCHumanReadable: `${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`, + color: faker.random.arrayElement(["#9be9a8", "#40c463", "#30a14e", "#216e39"]), + 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"]), + alert: faker.random.arrayElement(["Normal", "Urgent High", "Urgent Low", "High", "Low"]), + arrowHumanReadable: faker.random.arrayElement(["↑↑", "↑", "↗", "→", "↘", "↓", "↓↓"]), + })), + }, + }) + : null), + //Pagespeed + ...(set.plugins.enabled.pagespeed + ? ({ + pagespeed: { + url: options["pagespeed.url"] || "(attached website url)", + detailed: options["pagespeed.detailed"] || false, + scores: [ + { score: faker.datatype.float({ max: 1 }), title: "Performance" }, + { score: faker.datatype.float({ max: 1 }), title: "Accessibility" }, + { score: faker.datatype.float({ max: 1 }), title: "Best Practices" }, + { score: faker.datatype.float({ max: 1 }), title: "SEO" }, + ], + metrics: { + 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(), + }, + screenshot: options["pagespeed.screenshot"] ? "data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" : null, + }, + }) + : null), + //Projects + ...(set.plugins.enabled.projects + ? ({ + projects: { + totalCount: options["projects.limit"] + faker.datatype.number(10), + descriptions: options["projects.descriptions"], + list: new Array(Number(options["projects.limit"])).fill(null).map(_ => ({ + name: faker.lorem.sentence(), + description: faker.lorem.paragraph(), + updated: `${2 + faker.datatype.number(8)} days ago`, + progress: { + enabled: true, + todo: faker.datatype.number(50), + doing: faker.datatype.number(50), + done: faker.datatype.number(50), + get total() { + return this.todo + this.doing + this.done + }, + }, + })), + }, + }) + : null), + //Posts + ...(set.plugins.enabled.posts + ? ({ + posts: { + source: options["posts.source"], + descriptions: options["posts.descriptions"], + covers: options["posts.covers"], + list: new Array(Number(options["posts.limit"])).fill(null).map(_ => ({ + title: faker.lorem.sentence(), + description: faker.lorem.paragraph(), + date: faker.date.recent(), + image: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + })), + }, + }) + : null), + //Topics + ...(set.plugins.enabled.topics + ? ({ + topics: { + mode: options["topics.mode"], + list: new Array(Number(options["topics.limit"]) || 20).fill(null).map(_ => ({ + name: faker.lorem.words(2), + description: faker.lorem.sentence(), + icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + })), + }, + }) + : null), + //Stars + ...(set.plugins.enabled.stars + ? ({ + stars: { + repositories: [ + { + starredAt: faker.date.recent(), + 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" }, + }, + starred: "1 day ago", + }, + ...new Array(Number(options["stars.limit"]) - 1).fill(null).map((_, i) => ({ + starredAt: faker.date.recent(), + node: { + description: faker.lorem.sentence(), + forkCount: faker.datatype.number(100), + isFork: faker.datatype.boolean(), + issues: { + totalCount: faker.datatype.number(100), + }, + nameWithOwner: `${faker.random.word()}/${faker.random.word()}`, + openGraphImageUrl: faker.internet.url(), + pullRequests: { + totalCount: faker.datatype.number(100), + }, + stargazerCount: faker.datatype.number(10000), + licenseInfo: { nickname: null, name: "License" }, + primaryLanguage: { color: faker.internet.color(), name: faker.lorem.word() }, + }, + starred: `${i + 2} days ago`, + })), + ], + }, + }) + : null), + //Stars + ...(set.plugins.enabled.stargazers + ? ({ + get stargazers() { + const dates = [] + let total = faker.datatype.number(1000) + const result = { + total: { + dates: {}, + get max() { + return Math.max(...dates.map(date => this.dates[date])) + }, + get min() { + return Math.min(...dates.map(date => this.dates[date])) + }, + }, + increments: { + dates: {}, + get max() { + return Math.max(...dates.map(date => this.dates[date])) + }, + get min() { + return Math.min(...dates.map(date => this.dates[date])) + }, + }, + months: ["", "Jan.", "Feb.", "Mar.", "Apr.", "May", "June", "July", "Aug.", "Sep.", "Oct.", "Nov.", "Dec."], + } + for (let d = -14; d <= 0; d++) { + const date = new Date(Date.now() - d * 24 * 60 * 60 * 1000).toISOString().substring(0, 10) + dates.push(date) + result.total.dates[date] = (total += (result.increments.dates[date] = faker.datatype.number(100))) + } + return result + }, + }) + : null), + //Wakatime + ...(set.plugins.enabled.wakatime + ? ({ + get wakatime() { + const stats = array => { + const elements = [] + let results = new Array(4 + faker.datatype.number(2)).fill(null).map(_ => ({ + name: array ? faker.random.arrayElement(array) : faker.random.words(2).replace(/ /g, "-").toLocaleLowerCase(), + percent: 0, + total_seconds: faker.datatype.number(1000000), + })) + let percents = 100 + for (const result of results) { + result.percent = 1 + faker.datatype.number(percents - 1) + percents -= result.percent + result.percent /= 100 + } + results.filter(({ name }) => elements.includes(name) ? false : (elements.push(name), true)) + return results.sort((a, b) => b.percent - a.percent) + } + return { + sections: options["wakatime.sections"].split(",").map(x => x.trim()).filter(x => x), + days: Number(options["wakatime.days"]) || 7, + time: { total: faker.datatype.number(100000), daily: faker.datatype.number(24) }, + editors: stats(["VS Code", "Chrome", "IntelliJ", "PhpStorm", "WebStorm", "Android Studio", "Visual Studio", "Sublime Text", "PyCharm", "Vim", "Atom", "Xcode"]), + languages: stats(["JavaScript", "TypeScript", "PHP", "Java", "Python", "Vue.js", "HTML", "C#", "JSON", "Dart", "SCSS", "Kotlin", "JSX", "Go", "Ruby", "YAML"]), + projects: stats(), + os: stats(["Mac", "Windows", "Linux"]), + } + }, + }) + : null), + //Anilist + ...(set.plugins.enabled.anilist + ? ({ + anilist: { + user: { + stats: { + 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() })), + }, + }, + genres: new Array(4).fill(null).map(_ => ({ genre: faker.lorem.word() })), + }, + get lists() { + const media = type => ({ + name: faker.lorem.words(), + type, + status: faker.random.arrayElement(["FINISHED", "RELEASING", "NOT_YET_RELEASED", "CANCELLED", "HIATUS"]), + release: faker.date.past(20).getFullYear(), + genres: new Array(6).fill(null).map(_ => faker.lorem.word()), + progress: faker.datatype.number(100), + description: faker.lorem.paragraphs(), + scores: { user: faker.datatype.number(100), community: faker.datatype.number(100) }, + released: 100 + faker.datatype.number(1000), + artwork: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + }) + const sections = options["anilist.sections"].split(",").map(x => x.trim()).filter(x => x) + const medias = options["anilist.medias"].split(",").map(x => x.trim()).filter(x => x) + return { + ...(medias.includes("anime") + ? { + anime: { + ...(sections.includes("watching") ? { watching: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("ANIME")) } : {}), + ...(sections.includes("favorites") ? { favorites: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("ANIME")) } : {}), + }, + } + : {}), + ...(medias.includes("manga") + ? { + manga: { + ...(sections.includes("reading") ? { reading: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("MANGA")) } : {}), + ...(sections.includes("favorites") ? { favorites: new Array(Number(options["anilist.limit"]) || 4).fill(null).map(_ => media("MANGA")) } : {}), + }, + } + : {}), + } + }, + characters: new Array(11).fill(null).map(_ => ({ + name: faker.name.findName(), + artwork: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + })), + sections: options["anilist.sections"].split(",").map(x => x.trim()).filter(x => x), + }, + }) + : null), + //Activity + ...(set.plugins.enabled.activity + ? ({ + activity: { + timestamps: options["activity.timestamps"], + events: new Array(Number(options["activity.limit"])).fill(null).map(_ => + [ + { + type: "push", + repo: `${faker.random.word()}/${faker.random.word()}`, + size: 1, + branch: "master", + commits: [{ sha: faker.git.shortSha(), message: faker.lorem.sentence() }], + timestamp: faker.date.recent(), + }, + { + type: "comment", + on: "commit", + repo: `${faker.random.word()}/${faker.random.word()}`, + content: faker.lorem.paragraph(), + user: set.user, + mobile: null, + number: faker.git.shortSha(), + title: "", + timestamp: faker.date.recent(), + }, + { + type: "comment", + on: "pr", + repo: `${faker.random.word()}/${faker.random.word()}`, + content: faker.lorem.sentence(), + user: set.user, + mobile: null, + number: faker.datatype.number(100), + title: faker.lorem.sentence(), + timestamp: faker.date.recent(), + }, + { + type: "comment", + on: "issue", + repo: `${faker.random.word()}/${faker.random.word()}`, + content: faker.lorem.sentence(), + user: set.user, + mobile: null, + number: faker.datatype.number(100), + title: faker.lorem.sentence(), + timestamp: faker.date.recent(), + }, + { + type: "issue", + repo: `${faker.random.word()}/${faker.random.word()}`, + action: faker.random.arrayElement(["opened", "closed", "reopened"]), + user: set.user, + number: faker.datatype.number(100), + title: faker.lorem.sentence(), + timestamp: faker.date.recent(), + }, + { + type: "pr", + repo: `${faker.random.word()}/${faker.random.word()}`, + action: faker.random.arrayElement(["opened", "closed"]), + user: set.user, + number: faker.datatype.number(100), + title: faker.lorem.sentence(), + lines: { added: faker.datatype.number(1000), deleted: faker.datatype.number(1000) }, + files: { changed: faker.datatype.number(10) }, + timestamp: faker.date.recent(), + }, + { + type: "wiki", + repo: `${faker.random.word()}/${faker.random.word()}`, + pages: [faker.lorem.sentence(), faker.lorem.sentence()], + timestamp: faker.date.recent(), + }, + { + type: "fork", + repo: `${faker.random.word()}/${faker.random.word()}`, + timestamp: faker.date.recent(), + }, + { + type: "review", + repo: `${faker.random.word()}/${faker.random.word()}`, + user: set.user, + number: faker.datatype.number(100), + title: faker.lorem.sentence(), + timestamp: faker.date.recent(), + }, + { + type: "release", + repo: `${faker.random.word()}/${faker.random.word()}`, + action: "published", + name: faker.random.words(4), + draft: faker.datatype.boolean(), + prerelease: faker.datatype.boolean(), + timestamp: faker.date.recent(), + }, + { + type: "ref/create", + repo: `${faker.random.word()}/${faker.random.word()}`, + ref: { name: faker.lorem.slug(), type: faker.random.arrayElement(["tag", "branch"]) }, + timestamp: faker.date.recent(), + }, + { + type: "ref/delete", + repo: `${faker.random.word()}/${faker.random.word()}`, + ref: { name: faker.lorem.slug(), type: faker.random.arrayElement(["tag", "branch"]) }, + timestamp: faker.date.recent(), + }, + { + type: "member", + repo: `${faker.random.word()}/${faker.random.word()}`, + user: set.user, + timestamp: faker.date.recent(), + }, + { + type: "public", + repo: `${faker.random.word()}/${faker.random.word()}`, + timestamp: faker.date.recent(), + }, + { + type: "star", + repo: `${faker.random.word()}/${faker.random.word()}`, + action: "started", + timestamp: faker.date.recent(), + }, + ][Math.floor(Math.random() * 15)] + ), + }, + }) + : null), + //Isocalendar + ...(set.plugins.enabled.isocalendar + ? ({ + isocalendar: { + streak: { max: 30 + faker.datatype.number(20), current: faker.datatype.number(30) }, + max: 10 + faker.datatype.number(40), + average: faker.datatype.float(10), + svg: "(isometric calendar is not displayed in placeholder)", + duration: options["isocalendar.duration"], + }, + }) + : null), + //Stackoverflow + ...(set.plugins.enabled.stackoverflow + ? ({ + stackoverflow: { + sections: options["stackoverflow.sections"].split(",").map(x => x.trim()).filter(x => x), + lines: options["stackoverflow.lines"], + user: { + reputation: faker.datatype.number(100000), + badges: faker.datatype.number(1000), + questions: faker.datatype.number(1000), + answers: faker.datatype.number(1000), + comments: faker.datatype.number(1000), + views: faker.datatype.number(1000), + }, + "answers-top": new Array(options["stackoverflow.limit"]).fill(null).map(_ => ({ + type: "answer", + body: faker.lorem.paragraphs(), + score: faker.datatype.number(1000), + upvotes: faker.datatype.number(1000), + downvotes: faker.datatype.number(1000), + accepted: faker.datatype.boolean(), + comments: faker.datatype.number(1000), + author: set.user, + created: "01/01/1970", + link: null, + id: faker.datatype.number(100000), + question_id: faker.datatype.number(100000), + question: { + title: faker.lorem.sentence(), + tags: [faker.lorem.slug(), faker.lorem.slug()], + }, + })), + get ["answers-recent"]() { + return this["answers-top"] + }, + "questions-top": new Array(options["stackoverflow.limit"]).fill(null).map(_ => ({ + type: "question", + title: faker.lorem.sentence(), + body: faker.lorem.paragraphs(), + score: faker.datatype.number(1000), + upvotes: faker.datatype.number(1000), + downvotes: faker.datatype.number(1000), + favorites: faker.datatype.number(1000), + tags: [faker.lorem.slug(), faker.lorem.slug()], + answered: faker.datatype.boolean(), + answers: faker.datatype.number(1000), + comments: faker.datatype.number(1000), + views: faker.datatype.number(1000), + author: set.user, + created: "01/01/1970", + link: null, + id: faker.datatype.number(100000), + accepted_answer_id: faker.datatype.number(100000), + answer: null, + })), + get ["questions-recent"]() { + return this["questions-top"] + }, + }, + }) + : null), + }, } + //Formatters + data.f.bytes = function(n) { + for (const { u, v } of [{ u: "E", v: 10 ** 18 }, { u: "P", v: 10 ** 15 }, { u: "T", v: 10 ** 12 }, { u: "G", v: 10 ** 9 }, { u: "M", v: 10 ** 6 }, { u: "k", v: 10 ** 3 }]) { + if (n / v >= 1) + return `${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B` + } + return `${n} byte${n > 1 ? "s" : ""}` + } + data.f.percentage = function(n, { rescale = true } = {}) { + return `${ + (n * (rescale ? 100 : 1)).toFixed(2) + .replace(/[.]([1-9]*)(0+)$/, (m, a, b) => `.${a}`) + .replace(/[.]$/, "") + }%` + } + data.f.ellipsis = function(text, { length = 20 } = {}) { + text = `${text}` + if (text.length < length) + return text + return `${text.substring(0, length)}…` + } + data.f.date = function(string, options) { + return new Intl.DateTimeFormat("en-GB", options).format(new Date(string)) + } + //Render + return await ejs.render(image, data, { async: true, rmWhitespace: true }) + } //Reset globals contexts - globalThis.placeholder.init = function(globals) { - axios = globals.axios || axios - faker = globals.faker || faker - ejs = globals.ejs || ejs - } + globalThis.placeholder.init = function(globals) { + axios = globals.axios || axios + faker = globals.faker || faker + ejs = globals.ejs || ejs + } })() diff --git a/source/plugins/achievements/index.mjs b/source/plugins/achievements/index.mjs index 4dbc0e06..ee0439f7 100644 --- a/source/plugins/achievements/index.mjs +++ b/source/plugins/achievements/index.mjs @@ -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(/(?\d+)M\s+(?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 - } \ No newline at end of file +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(/(?\d+)M\s+(?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 +} diff --git a/source/plugins/achievements/list/index.mjs b/source/plugins/achievements/list/index.mjs index 5fa5e266..3939641a 100644 --- a/source/plugins/achievements/list/index.mjs +++ b/source/plugins/achievements/list/index.mjs @@ -1,3 +1,3 @@ //Exports - export {default as user} from "./users.mjs" - export {default as organization} from "./organizations.mjs" \ No newline at end of file +export {default as organization} from "./organizations.mjs" +export {default as user} from "./users.mjs" diff --git a/source/plugins/achievements/list/organizations.mjs b/source/plugins/achievements/list/organizations.mjs index 4f94e734..0efc7b93 100644 --- a/source/plugins/achievements/list/organizations.mjs +++ b/source/plugins/achievements/list/organizations.mjs @@ -1,140 +1,158 @@ /**Achievements list for users accounts */ - export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) { +export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) { + //Initialization + 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} - //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:'', + ...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"}), + }) + } - //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:"", - ...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:'', + ...rank(value, [1, 10, 30, 50]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - //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:"", - ...rank(value, [1, 10, 30, 50]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Managers + { + const value = organization.projects.totalCount + const unlock = organization.projects.nodes?.shift() - //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:'', + ...rank(value, [1, 2, 4, 8]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Managers", - text:`Created ${value} user project${imports.s(value)}`, - icon:"", - ...rank(value, [1, 2, 4, 8]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Packagers + { + const value = organization.packages.totalCount + const unlock = organization.packages.nodes?.shift() - //Packagers - { - const value = organization.packages.totalCount - const unlock = organization.packages.nodes?.shift() + list.push({ + title:"Packagers", + text:`Created ${value} package${imports.s(value)}`, + icon:'', + ...rank(value, [1, 20, 50, 100]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Packagers", - text:`Created ${value} package${imports.s(value)}`, - icon:"", - ...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 - //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:'', + ...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"}), + }) + } - list.push({ - title:"Maintainers", - text:`Maintaining a repository with ${value} star${imports.s(value)}`, - icon:"", - ...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:'', + ...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"}), + }) + } - //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:"", - ...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 - //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:'', + ...rank(value, [1, 8, 16, 32]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Polyglots", - text:`Using ${value} different programming language${imports.s(value)}`, - icon:"", - ...rank(value, [1, 8, 16, 32]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Sponsors + { + const value = organization.sponsorshipsAsSponsor.totalCount + const unlock = null - //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:'', + ...rank(value, [1, 5, 10, 20]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Sponsors", - text:`Sponsoring ${value} user${imports.s(value)} or organization${imports.s(value)}`, - icon:"", - ...rank(value, [1, 5, 10, 20]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Organization + { + const value = organization.membersWithRole.totalCount + const unlock = null - //Organization - { - const value = organization.membersWithRole.totalCount - const unlock = null + list.push({ + title:"Organization", + text:`Has ${value} member${imports.s(value)}`, + icon:'', + ...rank(value, [1, 100, 500, 1000]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Organization", - text:`Has ${value} member${imports.s(value)}`, - icon:"", - ...rank(value, [1, 100, 500, 1000]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Member + { + const value = computed.registered.diff + const unlock = null - //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:"", - ...rank(value, [1, 3, 5, 10]), value, unlock:new Date(unlock?.createdAt), - }) - } - - } \ No newline at end of file + list.push({ + title:"Member", + text:`Registered ${Math.floor(value)} year${imports.s(Math.floor(value))} ago`, + icon:'', + ...rank(value, [1, 3, 5, 10]), + value, + unlock:new Date(unlock?.createdAt), + }) + } +} diff --git a/source/plugins/achievements/list/users.mjs b/source/plugins/achievements/list/users.mjs index 178f8a7b..f71a51b3 100644 --- a/source/plugins/achievements/list/users.mjs +++ b/source/plugins/achievements/list/users.mjs @@ -1,284 +1,329 @@ /**Achievements list for users accounts */ - export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) { +export default async function({list, login, data, computed, imports, graphql, queries, rank, leaderboard}) { + //Initialization + 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} - //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:'', + ...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"}), + }) + } - //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:"", - ...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:'', + ...rank(value, [1, 5, 10, 20]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - //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:"", - ...rank(value, [1, 5, 10, 20]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Contributor + { + const value = user.pullRequests.totalCount + const unlock = user.pullRequests.nodes?.shift() - //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:'', + ...rank(value, [1, 200, 500, 1000]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Contributor", - text:`Opened ${value} pull request${imports.s(value)}`, - icon:"", - ...rank(value, [1, 200, 500, 1000]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Manager + { + const value = user.projects.totalCount + const unlock = user.projects.nodes?.shift() - //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:'', + ...rank(value, [1, 2, 3, 4]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Manager", - text:`Created ${value} user project${imports.s(value)}`, - icon:"", - ...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() - //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:'', + ...rank(value, [1, 200, 500, 1000]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Reviewer", - text:`Reviewed ${value} pull request${imports.s(value)}`, - icon:"", - ...rank(value, [1, 200, 500, 1000]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Packager + { + const value = user.packages.totalCount + const unlock = user.packages.nodes?.shift() - //Packager - { - const value = user.packages.totalCount - const unlock = user.packages.nodes?.shift() + list.push({ + title:"Packager", + text:`Created ${value} package${imports.s(value)}`, + icon:'', + ...rank(value, [1, 5, 10, 20]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Packager", - text:`Created ${value} package${imports.s(value)}`, - icon:"", - ...rank(value, [1, 5, 10, 20]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Scripter + { + const value = user.gists.totalCount + const unlock = user.gists.nodes?.shift() - //Scripter - { - const value = user.gists.totalCount - const unlock = user.gists.nodes?.shift() + list.push({ + title:"Scripter", + text:`Published ${value} gist${imports.s(value)}`, + icon:'', + ...rank(value, [1, 20, 50, 100]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Scripter", - text:`Published ${value} gist${imports.s(value)}`, - icon:"", - ...rank(value, [1, 20, 50, 100]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Worker + { + const value = user.organizations.totalCount + const unlock = user.organizations.nodes?.shift() - //Worker - { - const value = user.organizations.totalCount - const unlock = user.organizations.nodes?.shift() + list.push({ + title:"Worker", + text:`Joined ${value} organization${imports.s(value)}`, + icon:'', + ...rank(value, [1, 2, 4, 8]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Worker", - text:`Joined ${value} organization${imports.s(value)}`, - icon:"", - ...rank(value, [1, 2, 4, 8]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Stargazer + { + const value = user.starredRepositories.totalCount + const unlock = user.starredRepositories.nodes?.shift() - //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:'', + ...rank(value, [1, 200, 500, 1000]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Stargazer", - text:`Starred ${value} repositor${imports.s(value, "y")}`, - icon:"", - ...rank(value, [1, 200, 500, 1000]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Follower + { + const value = user.following.totalCount + const unlock = user.following.nodes?.shift() - //Follower - { - const value = user.following.totalCount - const unlock = user.following.nodes?.shift() + list.push({ + title:"Follower", + text:`Following ${value} user${imports.s(value)}`, + icon:'', + ...rank(value, [1, 200, 500, 1000]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Follower", - text:`Following ${value} user${imports.s(value)}`, - icon:"", - ...rank(value, [1, 200, 500, 1000]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Influencer + { + const value = user.followers.totalCount + const unlock = user.followers.nodes?.shift() - //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:'', + ...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"}), + }) + } - list.push({ - title:"Influencer", - text:`Followed by ${value} user${imports.s(value)}`, - icon:"", - ...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 - //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:'', + ...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"}), + }) + } - list.push({ - title:"Maintainer", - text:`Maintaining a repository with ${value} star${imports.s(value)}`, - icon:"", - ...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:'', + ...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"}), + }) + } - //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:"", - ...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 - //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:'', + ...rank(value, [1, 4, 8, 16]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Polyglot", - text:`Using ${value} different programming language${imports.s(value)}`, - icon:"", - ...rank(value, [1, 4, 8, 16]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Member + { + const value = computed.registered.diff + const unlock = null - //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:'', + ...rank(value, [1, 3, 5, 10]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Member", - text:`Registered ${Math.floor(value)} year${imports.s(Math.floor(value))} ago`, - icon:"", - ...rank(value, [1, 3, 5, 10]), value, unlock:new Date(unlock?.createdAt), - }) - } + //Sponsors + { + const value = user.sponsorshipsAsSponsor.totalCount + const unlock = null - //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:'', + ...rank(value, [1, 3, 5, 10]), + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Sponsor", - text:`Sponsoring ${value} user${imports.s(value)} or organization${imports.s(value)}`, - icon:"", - ...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 - //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:'', + rank:value ? "$" : "X", + progress:value ? 1 : 0, + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Verified", - text:"Registered a GPG key to sign commits", - icon:"", - 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 - //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:'', + rank:value ? "$" : "X", + progress:value ? 1 : 0, + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Explorer", - text:"Starred a topic on GitHub Explore", - icon:"", - rank:value ? "$" : "X", progress:value ? 1 : 0, value, unlock:new Date(unlock?.createdAt), - }) - } + //Automater + { + const value = process.env.GITHUB_ACTIONS + const unlock = null - //Automater - { - const value = process.env.GITHUB_ACTIONS - const unlock = null + list.push({ + title:"Automater", + text:"Use GitHub Actions to automate profile updates", + icon:'', + rank:value ? "$" : "X", + progress:value ? 1 : 0, + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Automater", - text:"Use GitHub Actions to automate profile updates", - icon:"", - 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 - //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:'', + rank:(value) && (login === _login) ? "$" : "X", + progress:(value) && (login === _login) ? 1 : 0, + value, + unlock:new Date(unlock?.createdAt), + }) + } - list.push({ - title:"Infographile", - text:"Fervent supporter of metrics", - icon:"", - 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 - //Octonaut - { - const {user:{viewerIsFollowing:value}, viewer:{login:_login}} = await graphql(queries.achievements.octocat()) - const unlock = null - - list.push({ - title:"Octonaut", - text:"Following octocat", - icon:"", - rank:(value)&&(login === _login) ? "$" : "X", progress:(value)&&(login === _login) ? 1 : 0, value, unlock:new Date(unlock?.createdAt), - }) - } - - } \ No newline at end of file + list.push({ + title:"Octonaut", + text:"Following octocat", + icon:'', + rank:(value) && (login === _login) ? "$" : "X", + progress:(value) && (login === _login) ? 1 : 0, + value, + unlock:new Date(unlock?.createdAt), + }) + } +} diff --git a/source/plugins/activity/index.mjs b/source/plugins/activity/index.mjs index 6260dc83..e640573b 100644 --- a/source/plugins/activity/index.mjs +++ b/source/plugins/activity/index.mjs @@ -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.(?.*)/)?.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.(?.*)/)?.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}} + } +} diff --git a/source/plugins/anilist/index.mjs b/source/plugins/anilist/index.mjs index 43393644..9be9d48c 100644 --- a/source/plugins/anilist/index.mjs +++ b/source/plugins/anilist/index.mjs @@ -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(//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(//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 - } \ No newline at end of file +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 +} diff --git a/source/plugins/base/index.mjs b/source/plugins/base/index.mjs index 7644f9db..f8fba746 100644 --- a/source/plugins/base/index.mjs +++ b/source/plugins/base/index.mjs @@ -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)) + }, +} diff --git a/source/plugins/contributors/index.mjs b/source/plugins/contributors/index.mjs index bf59d3d8..3937fd62 100644 --- a/source/plugins/contributors/index.mjs +++ b/source/plugins/contributors/index.mjs @@ -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 } - } \ No newline at end of file + } + + //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}} + } +} diff --git a/source/plugins/core/index.mjs b/source/plugins/core/index.mjs index cc03526e..966a80d3 100644 --- a/source/plugins/core/index.mjs +++ b/source/plugins/core/index.mjs @@ -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[+](?\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(/(?\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[+](?\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(/(?\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 +} diff --git a/source/plugins/followup/index.mjs b/source/plugins/followup/index.mjs index fef5636e..52757e1e 100644 --- a/source/plugins/followup/index.mjs +++ b/source/plugins/followup/index.mjs @@ -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}} + } +} diff --git a/source/plugins/gists/index.mjs b/source/plugins/gists/index.mjs index 8cba9330..8e1311ff 100644 --- a/source/plugins/gists/index.mjs +++ b/source/plugins/gists/index.mjs @@ -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}} + } +} diff --git a/source/plugins/habits/index.mjs b/source/plugins/habits/index.mjs index 617f0ba4..44c568a3 100644 --- a/source/plugins/habits/index.mjs +++ b/source/plugins/habits/index.mjs @@ -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(/(?[\d.]+)%\s+(?[\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(/(?[\d.]+)%\s+(?[\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}} + } +} diff --git a/source/plugins/introduction/index.mjs b/source/plugins/introduction/index.mjs index 69a1a390..8a2ce6df 100644 --- a/source/plugins/introduction/index.mjs +++ b/source/plugins/introduction/index.mjs @@ -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}} - } - } \ No newline at end of file + //Results + return {mode:context.mode, title, text} + } + //Handle errors + catch (error) { + throw {error:{message:"An error occured", instance:error}} + } +} diff --git a/source/plugins/isocalendar/index.mjs b/source/plugins/isocalendar/index.mjs index acab4a38..1b2332c8 100644 --- a/source/plugins/isocalendar/index.mjs +++ b/source/plugins/isocalendar/index.mjs @@ -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 = ` - ${[1, 2].map(k => ` + ${ + [1, 2].map(k => ` - ${[..."RGB"].map(channel => ``).join("")} + ${[..."RGB"].map(channel => ``).join("")} - `) - .join("")} + ` + ) + .join("") + } ` - //Iterate through weeks - for (const week of calendar.weeks) { - svg += `` - j = 0 - //Iterate through days - for (const day of week.contributionDays) { - const ratio = day.contributionCount/max - svg += ` - + //Iterate through weeks + for (const week of calendar.weeks) { + svg += `` + j = 0 + //Iterate through days + for (const day of week.contributionDays) { + const ratio = day.contributionCount / max + svg += ` + - - + + ` - j++ - } - svg += "" - i++ - } - svg += "" + j++ + } + svg += "" + i++ + } + 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}} + } +} diff --git a/source/plugins/languages/index.mjs b/source/plugins/languages/index.mjs index 2bcbd515..c5a8ee6a 100644 --- a/source/plugins/languages/index.mjs +++ b/source/plugins/languages/index.mjs @@ -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}} + } +} diff --git a/source/plugins/licenses/index.mjs b/source/plugins/licenses/index.mjs index fc2b21d2..b41e95e1 100644 --- a/source/plugins/licenses/index.mjs +++ b/source/plugins/licenses/index.mjs @@ -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 } } - } \ No newline at end of file + } +} diff --git a/source/plugins/lines/index.mjs b/source/plugins/lines/index.mjs index 227e07b3..68eb1d35 100644 --- a/source/plugins/lines/index.mjs +++ b/source/plugins/lines/index.mjs @@ -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}} + } +} diff --git a/source/plugins/music/index.mjs b/source/plugins/music/index.mjs index 3168e93e..0d39a20a 100644 --- a/source/plugins/music/index.mjs +++ b/source/plugins/music/index.mjs @@ -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\("(?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\("(?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}} + } +} diff --git a/source/plugins/nightscout/index.mjs b/source/plugins/nightscout/index.mjs index 303586af..60c5e5bd 100644 --- a/source/plugins/nightscout/index.mjs +++ b/source/plugins/nightscout/index.mjs @@ -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) diff --git a/source/plugins/notable/index.mjs b/source/plugins/notable/index.mjs index e200cc0d..408cde1c 100644 --- a/source/plugins/notable/index.mjs +++ b/source/plugins/notable/index.mjs @@ -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}} - } - } \ No newline at end of file + //Results + return {contributions} + } + //Handle errors + catch (error) { + throw {error:{message:"An error occured", instance:error}} + } +} diff --git a/source/plugins/pagespeed/index.mjs b/source/plugins/pagespeed/index.mjs index 2c7e33af..189b5155 100644 --- a/source/plugins/pagespeed/index.mjs +++ b/source/plugins/pagespeed/index.mjs @@ -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: (?[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: (?[A-Z_]+)/)?.groups?.description ?? null + message = `API returned ${status}${description ? ` (${description})` : ""}` + error = error.response?.data ?? null + } + throw {error:{message, instance:error}} + } +} diff --git a/source/plugins/people/index.mjs b/source/plugins/people/index.mjs index 6b2a1f91..c2dd7ddf 100644 --- a/source/plugins/people/index.mjs +++ b/source/plugins/people/index.mjs @@ -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}} + } +} diff --git a/source/plugins/posts/index.mjs b/source/plugins/posts/index.mjs index fab4673f..a9685e1f 100644 --- a/source/plugins/posts/index.mjs +++ b/source/plugins/posts/index.mjs @@ -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}} + } +} diff --git a/source/plugins/projects/index.mjs b/source/plugins/projects/index.mjs index c8b1f449..b1deed0a 100644 --- a/source/plugins/projects/index.mjs +++ b/source/plugins/projects/index.mjs @@ -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(/(?[-\w]+)[/](?[-\w]+)[/]projects[/](?\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(/(?[-\w]+)[/](?[-\w]+)[/]projects[/](?\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}} + } +} diff --git a/source/plugins/reactions/index.mjs b/source/plugins/reactions/index.mjs index 8619ff37..ba4b4a4c 100644 --- a/source/plugins/reactions/index.mjs +++ b/source/plugins/reactions/index.mjs @@ -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}} - } - } \ No newline at end of file + //Results + return {list, comments:comments.length, details, days, twemoji:q["config.twemoji"]} + } + //Handle errors + catch (error) { + throw {error:{message:"An error occured", instance:error}} + } +} diff --git a/source/plugins/rss/index.mjs b/source/plugins/rss/index.mjs index 426b8231..a92da33d 100644 --- a/source/plugins/rss/index.mjs +++ b/source/plugins/rss/index.mjs @@ -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}} - } - } \ No newline at end of file + //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}} + } +} diff --git a/source/plugins/screenshot/index.mjs b/source/plugins/screenshot/index.mjs index d3d68c33..3279ec26 100644 --- a/source/plugins/screenshot/index.mjs +++ b/source/plugins/screenshot/index.mjs @@ -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}} - } - } \ No newline at end of file + //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}} + } +} diff --git a/source/plugins/skyline/index.mjs b/source/plugins/skyline/index.mjs index 51530e42..8c886572 100644 --- a/source/plugins/skyline/index.mjs +++ b/source/plugins/skyline/index.mjs @@ -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}} + } +} diff --git a/source/plugins/stackoverflow/index.mjs b/source/plugins/stackoverflow/index.mjs index 26ef9898..186dcec9 100644 --- a/source/plugins/stackoverflow/index.mjs +++ b/source/plugins/stackoverflow/index.mjs @@ -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(/\s*(? {4}[\s\S]+?)(?=(?:)|(?:\s*(? {4}[\s\S]+?)(?=(?:)|(?:
See documentation 🌍