This commit is contained in:
lowlighter
2022-01-15 21:21:11 -05:00
96 changed files with 3883 additions and 3830 deletions

View File

@@ -3,12 +3,12 @@ import core from "@actions/core"
import github from "@actions/github"
import octokit from "@octokit/graphql"
import fs from "fs/promises"
import processes from "child_process"
import paths from "path"
import sgit from "simple-git"
import processes from "child_process"
import mocks from "../../../tests/mocks/index.mjs"
import metrics from "../metrics/index.mjs"
import setup from "../metrics/setup.mjs"
import mocks from "../../../tests/mocks/index.mjs"
process.on("unhandledRejection", error => {
throw error
})
@@ -278,8 +278,8 @@ async function retry(func, {retries = 1, delay = 0} = {}) {
try {
await new Promise(async (solve, reject) => {
let stdout = ""
setTimeout(() => reject("Timeout while waiting for Insights webserver"), 5*60*1000)
const web = await processes.spawn("node", ["/metrics/source/app/web/index.mjs"], {env:{...process.env, NO_SETTINGS: true }})
setTimeout(() => reject("Timeout while waiting for Insights webserver"), 5 * 60 * 1000)
const web = await processes.spawn("node", ["/metrics/source/app/web/index.mjs"], {env:{...process.env, NO_SETTINGS:true}})
web.stdout.on("data", data => (console.debug(`web > ${data}`), stdout += data, /Server ready !/.test(stdout) ? solve() : null))
web.stderr.on("data", data => console.debug(`web > ${data}`))
})
@@ -339,7 +339,7 @@ async function retry(func, {retries = 1, delay = 0} = {}) {
info.break()
info.section("Saving")
info("Output condition", _output_condition)
if ((_output_condition === "data-changed")&&((committer.commit) || (committer.pr))) {
if ((_output_condition === "data-changed") && ((committer.commit) || (committer.pr))) {
const {svg} = await import("../metrics/utils.mjs")
let data = ""
await retry(async () => {
@@ -485,6 +485,7 @@ async function retry(func, {retries = 1, delay = 0} = {}) {
}
else
throw error
}
info("Pull request number", number)
}, {retries:retries_output_action, delay:retries_delay_output_action})
@@ -532,7 +533,7 @@ async function retry(func, {retries = 1, delay = 0} = {}) {
if (delay) {
info.break()
info("Delay before ending job", `${delay}s`)
await new Promise(solve => setTimeout(solve, delay*1000))
await new Promise(solve => setTimeout(solve, delay * 1000))
}
//Success

View File

@@ -78,11 +78,11 @@ export default async function metrics({login, q}, {graphql, rest, plugins, conf,
console.debug(`metrics/compute/${login} > json output`)
const cache = new WeakSet()
const rendered = JSON.parse(JSON.stringify(data, (key, value) => {
if ((value instanceof Set)||(Array.isArray(value)))
if ((value instanceof Set) || (Array.isArray(value)))
return [...value]
if (value instanceof Map)
return Object.fromEntries(value)
if ((typeof value === "object")&&(value)) {
if ((typeof value === "object") && (value)) {
if (cache.has(value))
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, cache.has(v) ? "[Circular]" : v]))
cache.add(value)
@@ -227,14 +227,23 @@ metrics.insights = async function({login}, {graphql, rest, conf}, {Plugins, Temp
"habits.days":7,
"habits.facts":false,
"habits.charts":true,
introduction:true
introduction:true,
}
const plugins = {
achievements:{enabled:true},
isocalendar:{enabled:true},
languages:{enabled:true, extras:false},
activity:{enabled:true, markdown:"extended"},
notable:{enabled:true},
followup:{enabled:true},
habits:{enabled:true, extras:false},
introduction:{enabled:true},
}
const plugins = {achievements:{enabled:true}, isocalendar:{enabled:true}, languages:{enabled:true, extras:false}, activity:{enabled:true, markdown:"extended"}, notable:{enabled:true}, followup:{enabled:true}, habits:{enabled:true, extras:false}, introduction:{enabled:true}}
return metrics({login, q}, {graphql, rest, plugins, conf, convert:"json"}, {Plugins, Templates})
}
//Metrics insights static render
metrics.insights.output = async function ({login, imports, conf}, {graphql, rest, Plugins, Templates}) {
metrics.insights.output = async function({login, imports, conf}, {graphql, rest, Plugins, Templates}) {
//Server
console.debug(`metrics/compute/${login} > insights`)
const server = `http://localhost:${conf.settings.port}`
@@ -248,7 +257,7 @@ metrics.insights.output = async function ({login, imports, conf}, {graphql, rest
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
await page.evaluate(async json => localStorage.setItem("local.metrics", json), json) //eslint-disable-line no-undef
await page.goto(`${server}/about/${login}?embed=1&localstorage=1`)
await page.waitForSelector(".container .user", {timeout:10*60*1000})
await page.waitForSelector(".container .user", {timeout:10 * 60 * 1000})
//Rendering
console.debug(`metrics/compute/${login} > insights > rendering data`)
@@ -266,4 +275,4 @@ metrics.insights.output = async function ({login, imports, conf}, {graphql, rest
</html>`
await browser.close()
return {mime:"text/html", rendered}
}
}

View File

@@ -1,10 +1,10 @@
//Imports
import fs from "fs"
import yaml from "js-yaml"
import {marked} from "marked"
import fetch from "node-fetch"
import path from "path"
import url from "url"
import fetch from "node-fetch"
import {marked} from "marked"
//Defined categories
const categories = ["core", "github", "social", "community"]
@@ -293,23 +293,25 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
` <td>${Object.entries(compatibility).filter(([_, value]) => value).map(([id]) => `<a href="/source/templates/${id}"><code>${templates[id].name ?? ""}</code></a>`).join(" ")}</td>`,
" </tr>",
" <tr>",
` <td>${[
meta.supports?.includes("user") ? "<code>👤 Users</code>" : "",
meta.supports?.includes("organization") ? "<code>👥 Organizations</code>" : "",
meta.supports?.includes("repository") ? "<code>📓 Repositories</code>" : ""
].filter(v => v).join(" ")}</td>`,
` <td>${
[
meta.supports?.includes("user") ? "<code>👤 Users</code>" : "",
meta.supports?.includes("organization") ? "<code>👥 Organizations</code>" : "",
meta.supports?.includes("repository") ? "<code>📓 Repositories</code>" : "",
].filter(v => v).join(" ")
}</td>`,
" </tr>",
" <tr>",
` <td>${[
...(meta.scopes ?? []).map(scope => `<code>🔑 ${{public_access:"(scopeless)"}[scope] ?? scope}</code>`),
...Object.entries(inputs).filter(([_, {type}]) => type === "token").map(([token]) => `<code>🗝 ${token}</code>`),
...(meta.scopes?.length ? ["read:org", "read:user", "repo"].map(scope => !meta.scopes.includes(scope) ? `<code>${scope} (optional)</code>` : null).filter(v => v) : [])
...(meta.scopes?.length ? ["read:org", "read:user", "repo"].map(scope => !meta.scopes.includes(scope) ? `<code>${scope} (optional)</code>` : null).filter(v => v) : []),
].filter(v => v).join(" ") || "<i>No tokens are required for this plugin</i>"}</td>`,
" </tr>",
" <tr>",
demos({colspan:2, wrap:name === "base", examples:meta.examples}),
" </tr>",
"</table>"
"</table>",
].join("\n")
//Options table
@@ -339,14 +341,14 @@ metadata.plugin = async function({__plugins, __templates, name, logger}) {
cell.push(`<i>(${Array.isArray(o.format) ? o.format[0] : o.format})</i>`)
if ("min" in o)
cell.push(`<i>(${o.min}`)
if (("min" in o)||("max" in o))
if (("min" in o) || ("max" in o))
cell.push(`${"min" in o ? "" : "<i>("}𝑥${"max" in o ? "" : ")</i>"}`)
if ("max" in o)
cell.push(`${o.max})</i>`)
cell.push("<br>")
if ("zero" in o)
cell.push(`<b>zero behaviour:</b> ${o.zero}</br>`)
if (("default" in o)&&(o.default !== "")) {
if (("default" in o) && (o.default !== "")) {
let text = o.default
if (o.default === ".user.login")
text = "<code>→ User login</code>"
@@ -414,26 +416,30 @@ metadata.template = async function({__templates, name, plugins, logger}) {
` <td>${Object.entries(compatibility).filter(([_, value]) => value).map(([id]) => `<a href="/source/plugins/${id}" title="${plugins[id].name}">${plugins[id].icon}</a>`).join(" ")}${meta.formats?.includes("markdown") ? " <code>✓ embed()</code>" : ""}</td>`,
" </tr>",
" <tr>",
` <td>${[
meta.supports?.includes("user") ? "<code>👤 Users</code>" : "",
meta.supports?.includes("organization") ? "<code>👥 Organizations</code>" : "",
meta.supports?.includes("repository") ? "<code>📓 Repositories</code>" : ""
].filter(v => v).join(" ")}</td>`,
` <td>${
[
meta.supports?.includes("user") ? "<code>👤 Users</code>" : "",
meta.supports?.includes("organization") ? "<code>👥 Organizations</code>" : "",
meta.supports?.includes("repository") ? "<code>📓 Repositories</code>" : "",
].filter(v => v).join(" ")
}</td>`,
" </tr>",
" <tr>",
` <td>${[
meta.formats?.includes("svg") ? "<code>*️⃣ SVG</code>" : "",
meta.formats?.includes("png") ? "<code>*️⃣ PNG</code>" : "",
meta.formats?.includes("jpeg") ? "<code>*️⃣ JPEG</code>" : "",
meta.formats?.includes("json") ? "<code>#️⃣ JSON</code>" : "",
meta.formats?.includes("markdown") ? "<code>🔠 Markdown</code>" : "",
meta.formats?.includes("markdown-pdf") ? "<code>🔠 Markdown (PDF)</code>" : "",
].filter(v => v).join(" ")}</td>`,
` <td>${
[
meta.formats?.includes("svg") ? "<code>*️⃣ SVG</code>" : "",
meta.formats?.includes("png") ? "<code>*️⃣ PNG</code>" : "",
meta.formats?.includes("jpeg") ? "<code>*️⃣ JPEG</code>" : "",
meta.formats?.includes("json") ? "<code>#️⃣ JSON</code>" : "",
meta.formats?.includes("markdown") ? "<code>🔠 Markdown</code>" : "",
meta.formats?.includes("markdown-pdf") ? "<code>🔠 Markdown (PDF)</code>" : "",
].filter(v => v).join(" ")
}</td>`,
" </tr>",
" <tr>",
demos({colspan:2, examples:meta.examples}),
" </tr>",
"</table>"
"</table>",
].join("\n")
//Result
@@ -448,9 +454,9 @@ metadata.template = async function({__templates, name, plugins, logger}) {
compatibility:{
...Object.fromEntries(Object.entries(compatibility).filter(([_, value]) => value)),
...Object.fromEntries(Object.entries(compatibility).filter(([_, value]) => !value).map(([key, value]) => [key, meta.formats?.includes("markdown") ? "embed" : value])),
base:true
base:true,
},
header
header,
},
check({q, account = "bypass", format = null}) {
//Support check
@@ -481,31 +487,33 @@ metadata.to = {
//Demo for main and individual readmes
function demos({colspan = null, wrap = false, examples = {}} = {}) {
if (("default1" in examples)&&("default2" in examples)) {
if (("default1" in examples) && ("default2" in examples)) {
return [
wrap ? '<td colspan="2"><table><tr>' : "",
'<td align="center">',
`<img src="${examples.default1}" alt=""></img>`,
`<img src="${examples.default1}" alt=""></img>`,
"</td>",
'<td align="center">',
`<img src="${examples.default2}" alt=""></img>`,
`<img src="${examples.default2}" alt=""></img>`,
"</td>",
wrap ? "</tr></table></td>" : "",
].filter(v => v).join("\n")
}
return [
` <td ${colspan ? `colspan="${colspan}"` : ""} align="center">`,
`${Object.entries(examples).map(([text, link]) => {
let img = `<img src="${link}" alt=""></img>`
if (text !== "default") {
const open = text.charAt(0) === "+" ? " open" : ""
text = open ? text.substring(1) : text
text = `${text.charAt(0).toLocaleUpperCase()}${text.substring(1)}`
img = `<details${open}><summary>${text}</summary>${img}</details>`
}
return ` ${img}`
}).join("\n")}`,
`${
Object.entries(examples).map(([text, link]) => {
let img = `<img src="${link}" alt=""></img>`
if (text !== "default") {
const open = text.charAt(0) === "+" ? " open" : ""
text = open ? text.substring(1) : text
text = `${text.charAt(0).toLocaleUpperCase()}${text.substring(1)}`
img = `<details${open}><summary>${text}</summary>${img}</details>`
}
return ` ${img}`
}).join("\n")
}`,
' <img width="900" height="1" alt="">',
" </td>"
" </td>",
].filter(v => v).join("\n")
}

View File

@@ -3,38 +3,38 @@ import fs from "fs/promises"
import prism_lang from "prismjs/components/index.js"
import axios from "axios"
import processes from "child_process"
import crypto from "crypto"
import {minify as csso} from "csso"
import emoji from "emoji-name-map"
import fss from "fs"
import GIFEncoder from "gifencoder"
import jimp from "jimp"
import linguist from "linguist-js"
import {marked} from "marked"
import minimatch from "minimatch"
import nodechartist from "node-chartist"
import fetch from "node-fetch"
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 purgecss from "purgecss"
import readline from "readline"
import rss from "rss-parser"
import htmlsanitize from "sanitize-html"
import git from "simple-git"
import SVGO from "svgo"
import twemojis from "twemoji-parser"
import url from "url"
import util from "util"
import fetch from "node-fetch"
import readline from "readline"
import emoji from "emoji-name-map"
import minimatch from "minimatch"
import crypto from "crypto"
import linguist from "linguist-js"
import purgecss from "purgecss"
import {minify as csso} from "csso"
import SVGO from "svgo"
import xmlformat from "xml-formatter"
prism_lang()
//Exports
export {axios, fs, git, jimp, opengraph, os, paths, processes, rss, url, fetch, util, emoji, minimatch}
export {axios, emoji, fetch, fs, git, jimp, minimatch, opengraph, os, paths, processes, rss, url, util}
/**Returns module __dirname */
export function __module(module) {
@@ -81,7 +81,7 @@ export function formatters({timeZone} = {}) {
}
/**Bytes formatter */
format.bytes = function (n) {
format.bytes = function(n) {
for (const {u, v} of [{u:"E", v:10 ** 18}, {u:"P", v:10 ** 15}, {u:"T", v:10 ** 12}, {u:"G", v:10 ** 9}, {u:"M", v:10 ** 6}, {u:"k", v:10 ** 3}]) {
if (n / v >= 1)
return `${(n / v).toFixed(2).substr(0, 4).replace(/[.]0*$/, "")} ${u}B`
@@ -90,7 +90,7 @@ export function formatters({timeZone} = {}) {
}
/**Percentage formatter */
format.percentage = function (n, {rescale = true} = {}) {
format.percentage = function(n, {rescale = true} = {}) {
return `${
(n * (rescale ? 100 : 1)).toFixed(2)
.replace(/(?<=[.])(?<decimal>[1-9]*)0+$/, "$<decimal>")
@@ -166,7 +166,7 @@ export async function chartist() {
}
/**Language analyzer (single file) */
export async function language({filename, patch, prefix = "", timeout = 20*1000}) {
export async function language({filename, patch, prefix = "", timeout = 20 * 1000}) {
const path = paths.join(os.tmpdir(), `${prefix}-${Math.random()}`.replace(/[^\w-]/g, ""))
return new Promise(async (solve, reject) => {
setTimeout(() => {
@@ -222,9 +222,9 @@ export async function run(command, options, {prefixed = true, log = true} = {})
}
/**Spawn command (use this to execute commands and process output on the fly) */
export async function spawn(command, args = [], options = {}, {prefixed = true, timeout = 300*1000, stdout} = {}) { //eslint-disable-line max-params
export async function spawn(command, args = [], options = {}, {prefixed = true, timeout = 300 * 1000, stdout} = {}) { //eslint-disable-line max-params
const prefix = {win32:"wsl"}[process.platform] ?? ""
if ((prefixed)&&(prefix)) {
if ((prefixed) && (prefix)) {
args.unshift(command)
command = prefix
}
@@ -403,7 +403,7 @@ export const svg = {
if (Number.isFinite(Number(absolute)))
padding.absolute[dimension] = Number(absolute)
if (Number.isFinite(Number(relative)))
padding[dimension] = 1 + Number(relative/100)
padding[dimension] = 1 + Number(relative / 100)
}
console.debug(`metrics/svg/resize > padding width*${padding.width}+${padding.absolute.width}, height*${padding.height}+${padding.absolute.height}`)
//Render through browser and resize height
@@ -433,7 +433,7 @@ export const svg = {
console.debug(`bounds after applying padding width=${width} (*${padding.width}+${padding.absolute.width}), height=${height} (*${padding.height}+${padding.absolute.height})`)
//Resize svg
if (document.querySelector("svg").getAttribute("height") === "auto")
console.debug("skipped height resizing because it was set to \"auto\"")
console.debug('skipped height resizing because it was set to "auto"')
else
document.querySelector("svg").setAttribute("height", height)
//Enable animations
@@ -575,8 +575,8 @@ export const svg = {
if (error)
throw new Error(`Could not optimize SVG: \n${error}`)
return optimized
}
}
},
},
}
/**Wait */

View File

@@ -6,9 +6,9 @@ import express from "express"
import ratelimit from "express-rate-limit"
import cache from "memory-cache"
import util from "util"
import mocks from "../../../tests/mocks/index.mjs"
import metrics from "../metrics/index.mjs"
import setup from "../metrics/setup.mjs"
import mocks from "../../../tests/mocks/index.mjs"
/**App */
export default async function({mock, nosettings} = {}) {

View File

@@ -31,8 +31,8 @@
//Plugins
(async () => {
const { data: plugins } = await axios.get("/.plugins")
this.plugins.list = plugins.filter(({name}) => metadata[name]?.supports.includes("user") || metadata[name]?.supports.includes("organization"))
const categories = [...new Set(this.plugins.list.map(({category}) => category))]
this.plugins.list = plugins.filter(({ name }) => metadata[name]?.supports.includes("user") || metadata[name]?.supports.includes("organization"))
const categories = [...new Set(this.plugins.list.map(({ category }) => category))]
this.plugins.categories = Object.fromEntries(categories.map(category => [category, this.plugins.list.filter(value => category === value.category)]))
})(),
//Base
@@ -68,8 +68,10 @@
tab: {
immediate: true,
handler(current) {
if (current === 'action') this.clipboard = new ClipboardJS('.copy-action')
else this.clipboard?.destroy()
if (current === "action")
this.clipboard = new ClipboardJS(".copy-action")
else
this.clipboard?.destroy()
},
},
palette: {
@@ -181,7 +183,7 @@
scopes() {
return new Set([
...Object.entries(this.plugins.enabled).filter(([key, value]) => (key !== "base") && (value)).flatMap(([key]) => metadata[key].scopes),
...(Object.entries(this.plugins.enabled.base).filter(([key, value]) => value).length ? metadata.base.scopes : [])
...(Object.entries(this.plugins.enabled.base).filter(([key, value]) => value).length ? metadata.base.scopes : []),
])
},
//GitHub action auto-generated code
@@ -201,17 +203,19 @@
` steps:`,
` - uses: lowlighter/metrics@latest`,
` with:`,
...(this.scopes.size ? [
` # Your GitHub token`,
` # The following scopes are required:`,
...[...this.scopes].map(scope => ` # - ${scope}${scope === "public_access" ? " (default scope)" : ""}`),
` # The following additional scopes may be required:`,
` # - read:org (for organization related metrics)`,
` # - read:user (for user related data)`,
` # - repo (optional, if you want to include private repositories)`
] : [
` # Current configuration doesn't require a GitHub token`,
]),
...(this.scopes.size
? [
` # Your GitHub token`,
` # The following scopes are required:`,
...[...this.scopes].map(scope => ` # - ${scope}${scope === "public_access" ? " (default scope)" : ""}`),
` # The following additional scopes may be required:`,
` # - read:org (for organization related metrics)`,
` # - read:user (for user related data)`,
` # - repo (optional, if you want to include private repositories)`,
]
: [
` # Current configuration doesn't require a GitHub token`,
]),
` token: ${this.scopes.size ? `${"$"}{{ secrets.METRICS_TOKEN }}` : "NOT_NEEDED"}`,
``,
` # Options`,
@@ -252,7 +256,7 @@
methods: {
//Refresh computed properties
async refresh() {
const keys = {action:["scopes", "action"], markdown:["url", "embed"]}[this.tab]
const keys = { action: ["scopes", "action"], markdown: ["url", "embed"] }[this.tab]
if (keys) {
for (const key of keys)
this._computedWatchers[key]?.run()

View File

@@ -51,7 +51,7 @@
return await ejs.render(partial, data, { async: true, rmWhitespace: true })
},
//Meta-data
meta: { version: set.version, author: "lowlighter", generated:new Date().toGMTString().replace(/GMT$/g, "").trim() },
meta: { version: set.version, author: "lowlighter", generated: new Date().toGMTString().replace(/GMT$/g, "").trim() },
//Animated
animated: false,
//Display size
@@ -60,14 +60,14 @@
//Config
config: set.config,
//Extras
extras:{css:options["extras.css"] ?? ""},
extras: { css: options["extras.css"] ?? "" },
//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 }, about:{} },
licenses: { favorite: [""], used: { MIT: 1 }, about: {} },
token: { scopes: [] },
repositories: {
watchers: faker.datatype.number(1000),
@@ -193,7 +193,7 @@
drafts: faker.datatype.number(this.drafts),
skipped: faker.datatype.number(this.skipped),
}
}
},
},
pr: {
get count() {
@@ -210,7 +210,7 @@
merged: faker.datatype.number(this.skipped),
drafts: faker.datatype.number(this.drafts),
}
}
},
},
user: {
issues: {
@@ -232,7 +232,7 @@
drafts: faker.datatype.number(100),
},
},
indepth:options["followup.indepth"] ? {} : null
indepth: options["followup.indepth"] ? {} : null,
},
})
: null),
@@ -240,7 +240,10 @@
...(set.plugins.enabled.notable
? ({
notable: {
contributions: new Array(2 + faker.datatype.number(2)).fill(null).map(_ => ({ name: `${options["notable.repositories"] ? `${faker.lorem.slug()}/` : ""}${faker.lorem.slug()}`, avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==" })),
contributions: new Array(2 + faker.datatype.number(2)).fill(null).map(_ => ({
name: `${options["notable.repositories"] ? `${faker.lorem.slug()}/` : ""}${faker.lorem.slug()}`,
avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
})),
},
})
: null),
@@ -335,7 +338,7 @@
unlock: null,
text: faker.lorem.sentence(),
get icon() {
const colors = {S:["#FF0000", "#FF8500"], A:["#B59151", "#FFD576"], B:["#7D6CFF", "#B2A8FF"], C:["#2088FF", "#79B8FF"], $:["#FF48BD", "#FF92D8"], X:["#7A7A7A", "#B0B0B0"]}
const colors = { S: ["#FF0000", "#FF8500"], A: ["#B59151", "#FFD576"], B: ["#7D6CFF", "#B2A8FF"], C: ["#2088FF", "#79B8FF"], $: ["#FF48BD", "#FF92D8"], X: ["#7A7A7A", "#B0B0B0"] }
return `<g xmlns="http://www.w3.org/2000/svg" stroke-linecap="round" stroke-width="2" fill="none" fill-rule="evenodd"><g stroke-linejoin="round"><g stroke="#secondary"><path d="M8 43a3 3 0 100 6 3 3 0 000-6zm40 0a3.001 3.001 0 10.002 6.002A3.001 3.001 0 0048 43zm-18 3h-4.971m-11.045 0H11M45 46h-4"/></g><path stroke="#primary" d="M13 51h28M36.992 45.276l6.375-8.017c1.488.63 3.272.29 4.414-.977a3.883 3.883 0 00.658-4.193l-1.96 2.174-1.936-.151-.406-1.955 1.96-2.173a3.898 3.898 0 00-4.107 1.092 3.886 3.886 0 00-.512 4.485l-7.317 7.169c-1.32 1.314-.807 2.59-.236 3.105.67.601 1.888.845 3.067-.56z"/><g stroke="#primary"><path d="M12.652 31.063l9.442 12.578a.512.512 0 01-.087.716l-2.396 1.805a.512.512 0 01-.712-.114L9.46 33.47l-.176-3.557 3.37 1.15zM17.099 43.115l2.395-1.806"/></g></g><path d="M25.68 36.927v-2.54a2.227 2.227 0 01.37-1.265c-.526-.04-3.84-.371-3.84-4.302 0-1.013.305-1.839.915-2.477a4.989 4.989 0 01-.146-1.86c.087-.882.946-.823 2.577.178 1.277-.47 2.852-.47 4.725 0 .248-.303 2.434-1.704 2.658-.268.047.296.016.946-.093 1.95.516.524.776 1.358.78 2.501.007 2.261-1.26 3.687-3.8 4.278.24.436.355.857.346 1.264a117.57 117.57 0 000 2.614c2.43-.744 4.228-2.06 5.395-3.95.837-1.356 1.433-2.932 1.433-4.865 0-2.886-1.175-4.984-2.5-6.388C32.714 19.903 30.266 19 28 19a9.094 9.094 0 00-6.588 2.897C20.028 23.393 19 25.507 19 28.185c0 2.026.701 3.945 1.773 5.38 1.228 1.643 2.864 2.764 4.907 3.362zM52.98 25.002l-3.07 3.065-1.49-1.485M6.98 25.002l-3.07 3.065-1.49-1.485" stroke="#primary" stroke-linejoin="round"/><path d="M19.001 11V9a2 2 0 012-2h14a2 2 0 012 2v2m-21 12.028v-10.03a2 2 0 012-1.998h20a2 2 0 012 2v10.028" stroke="#secondary" stroke-linejoin="round"/><path stroke="#secondary" d="M28.001 7V3M15.039 7.797c-5.297 3.406-9.168 8.837-10.517 15.2m46.737-.936c-1.514-5.949-5.25-11.01-10.273-14.248"/></g>`
.replace(/#primary/g, colors[this.rank][0])
.replace(/#secondary/g, colors[this.rank][1])
@@ -362,20 +365,21 @@
})
: null),
//Code snippet
...(set.plugins.enabled.code
...(set.plugins.enabled.code
? ({
code: {
snippet: {
sha: faker.git.shortSha(),
message: faker.lorem.sentence(),
filename: 'docs/specifications.html',
filename: "docs/specifications.html",
status: "modified",
additions: faker.datatype.number(50),
deletions: faker.datatype.number(50),
patch: `<span class="token coord">@@ -0,0 +1,5 @@</span><br> //Imports<br><span class="token inserted">+ import app from "./src/app.mjs"</span><br><span class="token deleted">- import app from "./src/app.js"</span><br> //Start app<br> await app()<br>\\ No newline at end of file`,
patch:
`<span class="token coord">@@ -0,0 +1,5 @@</span><br> //Imports<br><span class="token inserted">+ import app from "./src/app.mjs"</span><br><span class="token deleted">- import app from "./src/app.js"</span><br> //Start app<br> await app()<br>\\ No newline at end of file`,
repo: `${faker.random.word()}/${faker.random.word()}`,
},
}
},
})
: null),
//Sponsors
@@ -392,10 +396,10 @@
count: faker.datatype.number(100),
goal: {
progress: faker.datatype.number(100),
title: `$${faker.datatype.number(100)*10} per month`,
description: "Invest in the software that powers your world"
}
}
title: `$${faker.datatype.number(100) * 10} per month`,
description: "Invest in the software that powers your world",
},
},
})
: null),
//Languages
@@ -412,7 +416,7 @@
get stats() {
return Object.fromEntries(Object.entries(this.favorites).map(([key, { value }]) => [key, value]))
},
["stats.recent"]:{
["stats.recent"]: {
total: faker.datatype.number(10000),
get lines() {
return Object.fromEntries(Object.entries(this.favorites).map(([key, { value }]) => [key, value]))
@@ -420,9 +424,9 @@
get stats() {
return Object.fromEntries(Object.entries(this.favorites).map(([key, { value }]) => [key, value]))
},
commits:faker.datatype.number(500),
files:faker.datatype.number(1000),
days:Number(options["languages.recent.days"])
commits: faker.datatype.number(500),
files: faker.datatype.number(1000),
days: Number(options["languages.recent.days"]),
},
favorites: distribution(7).map((value, index, array) => ({ name: faker.lorem.word(), color: faker.internet.color(), value, size: faker.datatype.number(1000000), x: array.slice(0, index).reduce((a, b) => a + b, 0) })),
recent: distribution(7).map((value, index, array) => ({ name: faker.lorem.word(), color: faker.internet.color(), value, size: faker.datatype.number(1000000), x: array.slice(0, index).reduce((a, b) => a + b, 0) })),
@@ -470,8 +474,8 @@
trim: options["habits.trim"],
lines: {
average: {
chars: faker.datatype.number(1000)/10,
}
chars: faker.datatype.number(1000) / 10,
},
},
commits: {
get hour() {
@@ -655,10 +659,10 @@
? ({
discussions: {
categories: {
stats: { '🙏 Q&A': faker.datatype.number(100), '📣 Announcements': faker.datatype.number(100), '💡 Ideas': faker.datatype.number(100), '💬 General': faker.datatype.number(100) },
favorite: '📣 Announcements'
stats: { "🙏 Q&A": faker.datatype.number(100), "📣 Announcements": faker.datatype.number(100), "💡 Ideas": faker.datatype.number(100), "💬 General": faker.datatype.number(100) },
favorite: "📣 Announcements",
},
upvotes: { discussions:faker.datatype.number(1000), comments: faker.datatype.number(1000) },
upvotes: { discussions: faker.datatype.number(1000), comments: faker.datatype.number(1000) },
started: faker.datatype.number(1000),
comments: faker.datatype.number(1000),
answers: faker.datatype.number(1000),
@@ -753,9 +757,11 @@
description: faker.lorem.sentence(),
count: faker.datatype.number(100),
repositories: new Array(Number(options["starlists.limit.repositories"])).fill(null).map((_, i) => ({
description: !i ? "📊 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 !" : faker.lorem.sentence(),
description: !i
? "📊 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 !"
: faker.lorem.sentence(),
name: !i ? "lowlighter/metrics" : `${faker.random.word()}/${faker.random.word()}`,
}))
})),
})),
},
})
@@ -1062,7 +1068,7 @@
? ({
support: {
stats: { solutions: faker.datatype.number(100), posts: faker.datatype.number(1000), topics: faker.datatype.number(1000), received: faker.datatype.number(1000), hearts: faker.datatype.number(1000) },
badges: { uniques: [ ], multiples: [], count: faker.datatype.number(1000) }
badges: { uniques: [], multiples: [], count: faker.datatype.number(1000) },
},
})
: null),
@@ -1070,7 +1076,7 @@
...(set.plugins.enabled.screenshot
? ({
screenshot: {
image:"data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
image: "data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
title: options["screenshot.title"],
height: 440,
width: 454,
@@ -1081,10 +1087,10 @@
...(set.plugins.enabled.skyline
? ({
skyline: {
animation:"data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
animation: "data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
width: 454,
height: 284,
compatibility: false
compatibility: false,
},
})
: null),
@@ -1175,11 +1181,11 @@
data.f.date = function(string, options) {
if (options.date) {
delete options.date
Object.assign(options, {day:"numeric", month:"short", year:"numeric"})
Object.assign(options, { day: "numeric", month: "short", year: "numeric" })
}
if (options.time) {
delete options.time
Object.assign(options, {hour:"2-digit", minute:"2-digit", second:"2-digit"})
Object.assign(options, { hour: "2-digit", minute: "2-digit", second: "2-digit" })
}
return new Intl.DateTimeFormat("en-GB", options).format(new Date(string))
}

View File

@@ -143,7 +143,7 @@ export default async function({list, login, data, computed, imports, graphql, qu
//Member
{
const { years: value } = computed.registered
const {years:value} = computed.registered
const unlock = null
list.push({

View File

@@ -219,7 +219,7 @@ export default async function({list, login, data, computed, imports, graphql, qu
//Member
{
const { years: value } = computed.registered
const {years:value} = computed.registered
const unlock = null
list.push({

View File

@@ -95,7 +95,7 @@ export default async function({login, graphql, rest, data, q, queries, imports},
}
catch (error) {
console.debug(`metrics/compute/${login}/base > failed to retrieve ${_batch} repositories after ${cursor}, this is probably due to an API timeout, halving batch`)
_batch = Math.floor(_batch/2)
_batch = Math.floor(_batch / 2)
if (_batch < 1) {
console.debug(`metrics/compute/${login}/base > failed to retrieve repositories, cannot halve batch anymore`)
throw error

View File

@@ -3,7 +3,7 @@ export default async function({login, q, imports, data, rest, account}, {enabled
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
if ((!enabled)||(!q.code))
if ((!enabled) || (!q.code))
return null
//Context
@@ -25,15 +25,22 @@ export default async function({login, q, imports, data, rest, account}, {enabled
try {
for (let page = 1; page <= pages; page++) {
console.debug(`metrics/compute/${login}/plugins > code > loading page ${page}/${pages}`)
events.push(...[...await Promise.all([...(context.mode === "repository" ? await rest.activity.listRepoEvents({owner:context.owner, repo:context.repo}) : await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data
.filter(({type}) => type === "PushEvent")
.filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase())
.filter(({repo:{name:repo}}) => !((skipped.includes(repo.split("/").pop())) || (skipped.includes(repo))))
.filter(event => visibility === "public" ? event.public : true)
.flatMap(({payload}) => Promise.all(payload.commits.map(async commit => (await rest.request(commit.url)).data)))])]
.flat()
.filter(({parents}) => parents.length <= 1)
.filter(({author}) => data.shared["commits.authoring"].filter(authoring => author?.login?.toLocaleLowerCase().includes(authoring)||author?.email?.toLocaleLowerCase().includes(authoring)||author?.name?.toLocaleLowerCase().includes(authoring)).length)
events.push(
...[
...await Promise.all([
...(context.mode === "repository"
? await rest.activity.listRepoEvents({owner:context.owner, repo:context.repo})
: await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data
.filter(({type}) => type === "PushEvent")
.filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase())
.filter(({repo:{name:repo}}) => !((skipped.includes(repo.split("/").pop())) || (skipped.includes(repo))))
.filter(event => visibility === "public" ? event.public : true)
.flatMap(({payload}) => Promise.all(payload.commits.map(async commit => (await rest.request(commit.url)).data))),
]),
]
.flat()
.filter(({parents}) => parents.length <= 1)
.filter(({author}) => data.shared["commits.authoring"].filter(authoring => author?.login?.toLocaleLowerCase().includes(authoring) || author?.email?.toLocaleLowerCase().includes(authoring) || author?.name?.toLocaleLowerCase().includes(authoring)).length),
)
}
}
@@ -48,8 +55,8 @@ export default async function({login, q, imports, data, rest, account}, {enabled
.filter(({patch}) => (patch ? (patch.match(/\n/mg)?.length ?? 1) : Infinity) < lines)
for (const file of files)
file.language = await imports.language({...file, prefix:login}).catch(() => "unknown")
files = files.filter(({language}) => (!languages.length)||(languages.includes(language.toLocaleLowerCase())))
const snippet = files[Math.floor(Math.random()*files.length)] ?? null
files = files.filter(({language}) => (!languages.length) || (languages.includes(language.toLocaleLowerCase())))
const snippet = files[Math.floor(Math.random() * files.length)] ?? null
if (snippet) {
//Trim common indent from content and change line feed
if (!snippet.patch.split("\n").shift().endsWith("@@"))
@@ -68,4 +75,4 @@ export default async function({login, q, imports, data, rest, account}, {enabled
catch (error) {
throw {error:{message:"An error occured", instance:error}}
}
}
}

View File

@@ -69,7 +69,7 @@ export default async function({login, q, imports, data, rest, graphql, queries,
//Contributions categories
const types = Object.fromEntries([...new Set(Object.keys(categories))].map(type => [type, new Set()]))
if ((sections.includes("categories"))&&(extras)) {
if ((sections.includes("categories")) && (extras)) {
//Temporary directory
const repository = `${repo.owner}/${repo.repo}`
const path = imports.paths.join(imports.os.tmpdir(), `${repository.replace(/[^\w]/g, "_")}`)
@@ -90,10 +90,11 @@ export default async function({login, q, imports, data, rest, graphql, queries,
stdout(line) {
if (line.trim().length)
files.push(line)
}
},
})
//Search for contributions type in specified categories
filesloop: for (const file of files) {
filesloop:
for (const file of files) {
for (const [category, globs] of Object.entries(categories)) {
for (const glob of [globs].flat(Infinity)) {
if (imports.minimatch(file, glob, {nocase:true})) {

View File

@@ -16,7 +16,13 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
}
//Init
const computed = {commits:0, sponsorships:0, licenses:{favorite:"", used:{}, about:{}}, token:{}, repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_closed:0, pr_merged:0, forks:0, forked:0, releases:0, deployments:0, environments:0}}
const computed = {
commits:0,
sponsorships:0,
licenses:{favorite:"", used:{}, about:{}},
token:{},
repositories:{watchers:0, stargazers:0, issues_open:0, issues_closed:0, pr_open:0, pr_closed:0, pr_merged:0, forks:0, forked:0, releases:0, deployments:0, environments:0},
}
const avatar = imports.imgb64(data.user.avatarUrl)
data.computed = computed
console.debug(`metrics/compute/${login} > formatting common metrics`)
@@ -38,6 +44,7 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
else if (process?.env?.TZ)
data.config.timezone = {name:process.env.TZ, offset}
//Display
data.large = display === "large"
data.columns = display === "columns"
@@ -101,7 +108,7 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
const months = diff.getUTCMonth() - new Date(0).getUTCMonth()
const days = diff.getUTCDate() - new Date(0).getUTCDate()
computed.registered = {years: years + days / 365.25, months}
computed.registered = {years:years + days / 365.25, months}
computed.registration = years ? `${years} year${imports.s(years)} ago` : months ? `${months} month${imports.s(months)} ago` : `${days} day${imports.s(days)} ago`
computed.cakeday = (years >= 1 && months === 0 && days === 0) ? true : false
@@ -124,7 +131,7 @@ export default async function({login, q}, {conf, data, rest, graphql, plugins, q
data.meta = {
version:conf.package.version,
author:conf.package.author,
generated:imports.format.date(new Date(), {date:true, time:true})
generated:imports.format.date(new Date(), {date:true, time:true}),
}
//Debug flags

View File

@@ -1,69 +1,69 @@
//Setup
export default async function({login, q, imports, graphql, queries, data, account}, {enabled = false} = {}) {
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
if ((!enabled)||(!q.discussions))
return null
export default async function({login, q, imports, graphql, queries, data, account}, {enabled = false} = {}) {
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
if ((!enabled) || (!q.discussions))
return null
//Load inputs
const {categories:_categories, "categories.limit":_categories_limit} = imports.metadata.plugins.discussions.inputs({data, account, q})
const discussions = {categories:{}, upvotes:{discussions:0, comments:0}}
discussions.display = {categories:_categories ? {limit:_categories_limit || Infinity} : null}
//Load inputs
const {categories:_categories, "categories.limit":_categories_limit} = imports.metadata.plugins.discussions.inputs({data, account, q})
const discussions = {categories:{}, upvotes:{discussions:0, comments:0}}
discussions.display = {categories:_categories ? {limit:_categories_limit || Infinity} : null}
//Fetch general statistics
const stats = Object.fromEntries(Object.entries((await graphql(queries.discussions.statistics({login}))).user).map(([key, value]) => [key, value.totalCount]))
Object.assign(discussions, stats)
//Fetch general statistics
const stats = Object.fromEntries(Object.entries((await graphql(queries.discussions.statistics({login}))).user).map(([key, value]) => [key, value.totalCount]))
Object.assign(discussions, stats)
//Load started discussions
{
const fetched = []
const categories = {}
let cursor = null
let pushed = 0
do {
console.debug(`metrics/compute/${login}/discussions > retrieving discussions after ${cursor}`)
const {user:{repositoryDiscussions:{edges = [], nodes = []} = {}}} = await graphql(queries.discussions.categories({login, after:cursor ? `after: "${cursor}"` : ""}))
cursor = edges?.[edges?.length - 1]?.cursor
fetched.push(...nodes)
pushed = nodes.length
console.debug(`metrics/compute/${login}/discussions > retrieved ${pushed} discussions after ${cursor}`)
} while ((pushed) && (cursor))
//Load started discussions
{
const fetched = []
const categories = {}
let cursor = null
let pushed = 0
do {
console.debug(`metrics/compute/${login}/discussions > retrieving discussions after ${cursor}`)
const {user:{repositoryDiscussions:{edges = [], nodes = []} = {}}} = await graphql(queries.discussions.categories({login, after:cursor ? `after: "${cursor}"` : ""}))
cursor = edges?.[edges?.length - 1]?.cursor
fetched.push(...nodes)
pushed = nodes.length
console.debug(`metrics/compute/${login}/discussions > retrieved ${pushed} discussions after ${cursor}`)
} while ((pushed) && (cursor))
//Compute upvotes
fetched.map(({upvoteCount}) => discussions.upvotes.discussions += upvoteCount)
//Compute upvotes
fetched.map(({upvoteCount}) => discussions.upvotes.discussions += upvoteCount)
//Compute favorite category
for (const category of [...fetched.map(({category:{emoji, name}}) => `${imports.emoji.get(emoji) ?? emoji} ${name}`)])
categories[category] = (categories[category] ?? 0) + 1
const categoryEntries = Object.entries(categories).sort((a, b) => b[1] - a[1])
discussions.categories.stats = Object.fromEntries(categoryEntries)
discussions.categories.favorite = categoryEntries[0]?.[0] ?? null
}
//Compute favorite category
for (const category of [...fetched.map(({category:{emoji, name}}) => `${imports.emoji.get(emoji) ?? emoji} ${name}`)])
categories[category] = (categories[category] ?? 0) + 1
const categoryEntries = Object.entries(categories).sort((a, b) => b[1] - a[1])
discussions.categories.stats = Object.fromEntries(categoryEntries)
discussions.categories.favorite = categoryEntries[0]?.[0] ?? null
}
//Load comments
{
const fetched = []
let cursor = null
let pushed = 0
do {
console.debug(`metrics/compute/${login}/discussions > retrieving comments after ${cursor}`)
const {user:{repositoryDiscussionComments:{edges = [], nodes = []} = {}}} = await graphql(queries.discussions.comments({login, after:cursor ? `after: "${cursor}"` : ""}))
cursor = edges?.[edges?.length - 1]?.cursor
fetched.push(...nodes)
pushed = nodes.length
console.debug(`metrics/compute/${login}/discussions > retrieved ${pushed} comments after ${cursor}`)
} while ((pushed) && (cursor))
//Load comments
{
const fetched = []
let cursor = null
let pushed = 0
do {
console.debug(`metrics/compute/${login}/discussions > retrieving comments after ${cursor}`)
const {user:{repositoryDiscussionComments:{edges = [], nodes = []} = {}}} = await graphql(queries.discussions.comments({login, after:cursor ? `after: "${cursor}"` : ""}))
cursor = edges?.[edges?.length - 1]?.cursor
fetched.push(...nodes)
pushed = nodes.length
console.debug(`metrics/compute/${login}/discussions > retrieved ${pushed} comments after ${cursor}`)
} while ((pushed) && (cursor))
//Compute upvotes
fetched.map(({upvoteCount}) => discussions.upvotes.comments += upvoteCount)
}
//Compute upvotes
fetched.map(({upvoteCount}) => discussions.upvotes.comments += upvoteCount)
}
//Results
return discussions
}
//Handle errors
catch (error) {
throw {error:{message:"An error occured", instance:error}}
}
//Results
return discussions
}
//Handle errors
catch (error) {
throw {error:{message:"An error occured", instance:error}}
}
}

View File

@@ -29,7 +29,7 @@ export default async function({login, data, computed, imports, q, graphql, queri
closed:0,
drafts:0,
skipped:0,
}
},
},
pr:{
get count() {
@@ -50,13 +50,12 @@ export default async function({login, data, computed, imports, q, graphql, queri
closed:0,
merged:0,
drafts:0,
}
},
},
}
//Extras features
if (extras) {
//Indepth mode
if (indepth) {
console.debug(`metrics/compute/${login}/plugins > followup > indepth`)
@@ -90,7 +89,7 @@ export default async function({login, data, computed, imports, q, graphql, queri
}
//Load user issues and pull requests
if ((account === "user")&&(sections.includes("user"))) {
if ((account === "user") && (sections.includes("user"))) {
const search = await graphql(queries.followup.user({login}))
followup.user = {
issues:{

View File

@@ -1,5 +1,5 @@
//Legacy import
import { recent as recent_analyzer } from "./../languages/analyzers.mjs"
import {recent as recent_analyzer} from "./../languages/analyzers.mjs"
//Setup
export default async function({login, data, rest, imports, q, account}, {enabled = false, extras = false, ...defaults} = {}) {
@@ -45,7 +45,7 @@ export default async function({login, data, rest, imports, q, account}, {enabled
...await Promise.allSettled(
commits
.flatMap(({payload}) => payload.commits)
.filter(({author}) => data.shared["commits.authoring"].filter(authoring => author?.login?.toLocaleLowerCase().includes(authoring)||author?.email?.toLocaleLowerCase().includes(authoring)||author?.name?.toLocaleLowerCase().includes(authoring)).length)
.filter(({author}) => data.shared["commits.authoring"].filter(authoring => author?.login?.toLocaleLowerCase().includes(authoring) || author?.email?.toLocaleLowerCase().includes(authoring) || author?.name?.toLocaleLowerCase().includes(authoring)).length)
.map(async commit => (await rest.request(commit)).data.files),
),
]
@@ -93,22 +93,23 @@ export default async function({login, data, rest, imports, q, account}, {enabled
//Compute average number of characters per line of code fetched
console.debug(`metrics/compute/${login}/plugins > habits > computing average number of characters per line of code`)
const lines = patches.flatMap(({patch}) => patch.split("\n").map(line => line.length))
habits.lines.average.chars = lines.reduce((a, b) => a + b, 0)/lines.length
habits.lines.average.chars = lines.reduce((a, b) => a + b, 0) / lines.length
}
//Linguist
if ((extras)&&(charts)) {
if ((extras) && (charts)) {
//Check if linguist exists
console.debug(`metrics/compute/${login}/plugins > habits > searching recently used languages using linguist`)
if (patches.length) {
//Call language analyzer (note: using content from other plugin is usually disallowed, this is mostly for legacy purposes)
habits.linguist.available = true
const {total, stats} = await recent_analyzer({login, data, imports, rest, account}, {days, load:from || 1000, tempdir:"habits"})
habits.linguist.languages = Object.fromEntries(Object.entries(stats).map(([language, value]) => [language, value/total]))
habits.linguist.languages = Object.fromEntries(Object.entries(stats).map(([language, value]) => [language, value / total]))
habits.linguist.ordered = Object.entries(habits.linguist.languages).sort(([_an, a], [_bn, b]) => b - a)
}
else
console.debug(`metrics/compute/${login}/plugins > habits > linguist not available`)
}
//Results

View File

@@ -114,4 +114,4 @@ async function statistics({login, graphql, queries, start, end, calendar}) {
//Compute average
average = (values.reduce((a, b) => a + b, 0) / values.length).toFixed(2).replace(/[.]0+$/, "")
return {streak, max, average}
}
}

View File

@@ -61,15 +61,16 @@ export async function recent({login, data, imports, rest, account}, {skipped = [
//Get user recent activity
console.debug(`metrics/compute/${login}/plugins > languages > querying api`)
const commits = [], pages = Math.ceil(load/100), results = {total:0, lines:{}, stats:{}, colors:{}, commits:0, files:0, missed:0, days}
const commits = [], pages = Math.ceil(load / 100), results = {total:0, lines:{}, stats:{}, colors:{}, commits:0, files:0, missed:0, days}
try {
for (let page = 1; page <= pages; page++) {
console.debug(`metrics/compute/${login}/plugins > languages > loading page ${page}`)
commits.push(...(await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data
.filter(({type}) => type === "PushEvent")
.filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase())
.filter(({repo:{name:repo}}) => (!skipped.includes(repo.toLocaleLowerCase())) && (!skipped.includes(repo.toLocaleLowerCase().split("/").pop())))
.filter(({created_at}) => new Date(created_at) > new Date(Date.now() - days * 24 * 60 * 60 * 1000))
commits.push(
...(await rest.activity.listEventsForAuthenticatedUser({username:login, per_page:100, page})).data
.filter(({type}) => type === "PushEvent")
.filter(({actor}) => account === "organization" ? true : actor.login?.toLocaleLowerCase() === login.toLocaleLowerCase())
.filter(({repo:{name:repo}}) => (!skipped.includes(repo.toLocaleLowerCase())) && (!skipped.includes(repo.toLocaleLowerCase().split("/").pop())))
.filter(({created_at}) => new Date(created_at) > new Date(Date.now() - days * 24 * 60 * 60 * 1000)),
)
}
}
@@ -86,17 +87,17 @@ export async function recent({login, data, imports, rest, account}, {skipped = [
...await Promise.allSettled(
commits
.flatMap(({payload}) => payload.commits)
.filter(({author}) => data.shared["commits.authoring"].filter(authoring => author?.login?.toLocaleLowerCase().includes(authoring)||author?.email?.toLocaleLowerCase().includes(authoring)||author?.name?.toLocaleLowerCase().includes(authoring)).length)
.filter(({author}) => data.shared["commits.authoring"].filter(authoring => author?.login?.toLocaleLowerCase().includes(authoring) || author?.email?.toLocaleLowerCase().includes(authoring) || author?.name?.toLocaleLowerCase().includes(authoring)).length)
.map(commit => commit.url)
.map(async commit => (await rest.request(commit)).data),
)
),
]
.filter(({status}) => status === "fulfilled")
.map(({value}) => value)
.filter(({parents}) => parents.length <= 1)
.map(({files}) => files)
.flatMap(files => files.map(file => ({name:imports.paths.basename(file.filename), directory:imports.paths.dirname(file.filename), patch:file.patch ?? "", repo:file.raw_url?.match(/(?<=^https:..github.com\/)(?<repo>.*)(?=\/raw)/)?.groups.repo ?? "_"})))
.map(({name, directory, patch, repo}) => ({name, directory:`${repo.replace(/[/]/g, "@")}/${directory}`, patch:patch.split("\n").filter(line => /^[+]/.test(line)).map(line => line.substring(1)).join("\n")}))
.filter(({status}) => status === "fulfilled")
.map(({value}) => value)
.filter(({parents}) => parents.length <= 1)
.map(({files}) => files)
.flatMap(files => files.map(file => ({name:imports.paths.basename(file.filename), directory:imports.paths.dirname(file.filename), patch:file.patch ?? "", repo:file.raw_url?.match(/(?<=^https:..github.com\/)(?<repo>.*)(?=\/raw)/)?.groups.repo ?? "_"})))
.map(({name, directory, patch, repo}) => ({name, directory:`${repo.replace(/[/]/g, "@")}/${directory}`, patch:patch.split("\n").filter(line => /^[+]/.test(line)).map(line => line.substring(1)).join("\n")}))
//Temporary directory
const path = imports.paths.join(imports.os.tmpdir(), `${data.user.databaseId}-${tempdir}`)
@@ -164,13 +165,13 @@ async function analyze({login, imports, data}, {results, path, categories = ["pr
console.debug(`metrics/compute/${login}/plugins > languages > indepth > checking git log`)
for (let page = 0; ; page++) {
try {
console.debug(`metrics/compute/${login}/plugins > languages > indepth > processing commits ${page*per_page} from ${(page+1)*per_page}`)
console.debug(`metrics/compute/${login}/plugins > languages > indepth > processing commits ${page * per_page} from ${(page + 1) * per_page}`)
let empty = true, file = null, lang = null
await imports.spawn("git", ["log", ...data.shared["commits.authoring"].map(authoring => `--author="${authoring}"`), "--regexp-ignore-case", "--format=short", "--patch", `--max-count=${per_page}`, `--skip=${page*per_page}`], {cwd:path}, {
await imports.spawn("git", ["log", ...data.shared["commits.authoring"].map(authoring => `--author="${authoring}"`), "--regexp-ignore-case", "--format=short", "--patch", `--max-count=${per_page}`, `--skip=${page * per_page}`], {cwd:path}, {
stdout(line) {
try {
//Unflag empty output
if ((empty)&&(line.trim().length))
if ((empty) && (line.trim().length))
empty = false
//Commits counter
if (/^commit [0-9a-f]{40}$/.test(line)) {
@@ -178,13 +179,13 @@ async function analyze({login, imports, data}, {results, path, categories = ["pr
return
}
//Ignore empty lines or unneeded lines
if ((!/^[+]/.test(line))||(!line.length))
if ((!/^[+]/.test(line)) || (!line.length))
return
//File marker
if (/^[+]{3}\sb[/](?<file>[\s\S]+)$/.test(line)) {
file = `${path}/${line.match(/^[+]{3}\sb[/](?<file>[\s\S]+)$/)?.groups?.file}`.replace(/\\/g, "/")
lang = files[file] ?? null
if ((lang)&&(!categories.includes(languageResults[lang].type)))
if ((lang) && (!categories.includes(languageResults[lang].type)))
lang = null
edited.add(file)
return
@@ -203,7 +204,7 @@ async function analyze({login, imports, data}, {results, path, categories = ["pr
catch (error) {
console.debug(`metrics/compute/${login}/plugins > languages > indepth > an error occured while processing line (${error.message}), skipping...`)
}
}
},
})
if (empty) {
console.debug(`metrics/compute/${login}/plugins > languages > indepth > no more commits`)
@@ -223,7 +224,7 @@ if (/languages.analyzers.mjs$/.test(process.argv[1])) {
(async function() {
//Parse inputs
const [_authoring, path] = process.argv.slice(2)
if ((!_authoring)||(!path)) {
if ((!_authoring) || (!path)) {
console.log("Usage is:\n npm run indepth -- <commits authoring> <repository local path>\n\n")
process.exit(1)
}
@@ -235,7 +236,7 @@ if (/languages.analyzers.mjs$/.test(process.argv[1])) {
//Prepare call
const imports = await import("../../app/metrics/utils.mjs")
const results = {total:0, lines:{}, colors:{}, stats:{}, missed:0}
console.debug = log => /exited with code null/.test(log) ? console.error(log.replace(/^.*--max-count=(?<step>\d+) --skip=(?<start>\d+).*$/, (_, step, start) => `error: skipped commits ${start} from ${Number(start)+Number(step)}`)) : null
console.debug = log => /exited with code null/.test(log) ? console.error(log.replace(/^.*--max-count=(?<step>\d+) --skip=(?<start>\d+).*$/, (_, step, start) => `error: skipped commits ${start} from ${Number(start) + Number(step)}`)) : null
//Analyze repository
console.log(`commits authoring | ${authoring}\nrepository path | ${path}\n`)

View File

@@ -17,7 +17,11 @@ export default async function({login, data, imports, q, rest, account}, {enabled
}
//Load inputs
let {ignored, skipped, colors, aliases, details, threshold, limit, indepth, "analysis.timeout":timeout, sections, categories, "recent.categories":_recent_categories, "recent.load":_recent_load, "recent.days":_recent_days} = imports.metadata.plugins.languages.inputs({data, account, q})
let {ignored, skipped, colors, aliases, details, threshold, limit, indepth, "analysis.timeout":timeout, sections, categories, "recent.categories":_recent_categories, "recent.load":_recent_load, "recent.days":_recent_days} = imports.metadata.plugins.languages.inputs({
data,
account,
q,
})
threshold = (Number(threshold.replace(/%$/, "")) || 0) / 100
skipped.push(...data.shared["repositories.skipped"])
if (!limit)
@@ -59,7 +63,7 @@ export default async function({login, data, imports, q, rest, account}, {enabled
//Extras features
if (extras) {
//Recently used languages
if ((sections.includes("recently-used"))&&(context.mode === "user")) {
if ((sections.includes("recently-used")) && (context.mode === "user")) {
try {
console.debug(`metrics/compute/${login}/plugins > languages > using recent analyzer`)
languages["stats.recent"] = await recent_analyzer({login, data, imports, rest, account}, {skipped, categories:_recent_categories ?? categories, days:_recent_days, load:_recent_load, timeout})
@@ -102,7 +106,8 @@ export default async function({login, data, imports, q, rest, account}, {enabled
//Compute languages stats
for (const {section, stats = {}, lines = {}, total = 0} of [{section:"favorites", stats:languages.stats, lines:languages.lines, total:languages.total}, {section:"recent", ...languages["stats.recent"]}]) {
console.debug(`metrics/compute/${login}/plugins > languages > computing stats ${section}`)
languages[section] = Object.entries(stats).filter(([name]) => !ignored.includes(name.toLocaleLowerCase())).sort(([_an, a], [_bn, b]) => b - a).slice(0, limit).map(([name, value]) => ({name, value, size:value, color:languages.colors[name], x:0})).filter(({value}) => value / total > threshold)
languages[section] = Object.entries(stats).filter(([name]) => !ignored.includes(name.toLocaleLowerCase())).sort(([_an, a], [_bn, b]) => b - a).slice(0, limit).map(([name, value]) => ({name, value, size:value, color:languages.colors[name], x:0})).filter(({value}) => value / total > threshold
)
const visible = {total:Object.values(languages[section]).map(({size}) => size).reduce((a, b) => a + b, 0)}
for (let i = 0; i < languages[section].length; i++) {
const {name} = languages[section][i]

View File

@@ -23,7 +23,8 @@ export default async function({login, data, imports, rest, q, account}, {enabled
//Get contributors stats from repositories
console.debug(`metrics/compute/${login}/plugins > lines > querying api`)
const lines = {added:0, deleted:0}
const response = [...await Promise.allSettled(repositories.map(({repo, owner}) => (skipped.includes(repo.toLocaleLowerCase())) || (skipped.includes(`${owner}/${repo}`.toLocaleLowerCase())) ? {} : rest.repos.getContributorsStats({owner, repo})))].filter(({status}) => status === "fulfilled").map(({value}) => value)
const response = [...await Promise.allSettled(repositories.map(({repo, owner}) => (skipped.includes(repo.toLocaleLowerCase())) || (skipped.includes(`${owner}/${repo}`.toLocaleLowerCase())) ? {} : rest.repos.getContributorsStats({owner, repo})))].filter(({status}) => status === "fulfilled"
).map(({value}) => value)
//Compute changed lines
console.debug(`metrics/compute/${login}/plugins > lines > computing total diff`)

View File

@@ -132,11 +132,11 @@ export default async function({login, imports, data, q, account}, {enabled = fal
//Parse tracklist
tracks = [
...await frame.evaluate(() => [...document.querySelectorAll("ytmusic-playlist-shelf-renderer ytmusic-responsive-list-item-renderer")].map(item => ({
name:item.querySelector("yt-formatted-string.title > a")?.innerText ?? "",
artist:item.querySelector(".secondary-flex-columns > yt-formatted-string > a")?.innerText ?? "",
artwork:item.querySelector("img").src,
})
)),
name:item.querySelector("yt-formatted-string.title > a")?.innerText ?? "",
artist:item.querySelector(".secondary-flex-columns > yt-formatted-string > a")?.innerText ?? "",
artwork:item.querySelector("img").src,
}))
),
]
break
}
@@ -257,12 +257,11 @@ export default async function({login, imports, data, q, account}, {enabled = fal
try {
//Request access token
console.debug(`metrics/compute/${login}/plugins > music > requesting access token with youtube refresh token`)
const res = await imports.axios.post("https://music.youtube.com/youtubei/v1/browse?alt=json&key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30",
{
const res = await imports.axios.post("https://music.youtube.com/youtubei/v1/browse?alt=json&key=AIzaSyC9XL3ZjWddXya6X74dJoCTL-WEYFDNX30", {
browseEndpointContextSupportedConfigs:{
browseEndpointContextMusicConfig:{
pageType:"MUSIC_PAGE_TYPE_PLAYLIST",
}
browseEndpointContextMusicConfig:{
pageType:"MUSIC_PAGE_TYPE_PLAYLIST",
},
},
context:{
client:{
@@ -272,9 +271,8 @@ export default async function({login, imports, data, q, account}, {enabled = fal
hl:"en",
},
},
browseId:"FEmusic_history"
},
{
browseId:"FEmusic_history",
}, {
headers:{
Authorization:SAPISIDHASH,
Cookie:token,
@@ -337,14 +335,14 @@ export default async function({login, imports, data, q, account}, {enabled = fal
Object.defineProperty(modes, "top", {
get() {
return `Top played artists ${time_msg}`
}
},
})
}
else {
Object.defineProperty(modes, "top", {
get() {
return `Top played tracks ${time_msg}`
}
},
})
}
@@ -355,7 +353,7 @@ export default async function({login, imports, data, q, account}, {enabled = fal
//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" } }
throw {error:{message:"Spotify token must contain client id/secret and refresh token"}}
else if (limit > 50)
throw {error:{message:"Spotify top limit cannot be greater than 50"}}
@@ -372,40 +370,39 @@ export default async function({login, imports, data, q, account}, {enabled = fal
//Retrieve tracks
console.debug(`metrics/compute/${login}/plugins > music > querying spotify api`)
tracks = []
const loaded =
top_type === "artists"
? (
await imports.axios.get(
`https://api.spotify.com/v1/me/top/artists?time_range=${time_range}_term&limit=${limit}`,
{
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${access}`,
},
}
)
).data.items.map(({ name, genres, images }) => ({
name,
artist: genres.join(" • "),
artwork: images[0].url,
}))
: (
await imports.axios.get(
`https://api.spotify.com/v1/me/top/tracks?time_range=${time_range}_term&limit=${limit}`,
{
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${access}`,
},
}
)
).data.items.map(({ name, artists, album }) => ({
name,
artist: artists[0].name,
artwork: album.images[0].url,
}))
const loaded = top_type === "artists"
? (
await imports.axios.get(
`https://api.spotify.com/v1/me/top/artists?time_range=${time_range}_term&limit=${limit}`,
{
headers:{
"Content-Type":"application/json",
Accept:"application/json",
Authorization:`Bearer ${access}`,
},
},
)
).data.items.map(({name, genres, images}) => ({
name,
artist:genres.join(" • "),
artwork:images[0].url,
}))
: (
await imports.axios.get(
`https://api.spotify.com/v1/me/top/tracks?time_range=${time_range}_term&limit=${limit}`,
{
headers:{
"Content-Type":"application/json",
Accept:"application/json",
Authorization:`Bearer ${access}`,
},
},
)
).data.items.map(({name, artists, album}) => ({
name,
artist:artists[0].name,
artwork:album.images[0].url,
}))
//Ensure no duplicate are added
for (const track of loaded) {
if (!tracks.map(({name}) => name).includes(track.name))
@@ -431,38 +428,37 @@ export default async function({login, imports, data, q, account}, {enabled = fal
try {
console.debug(`metrics/compute/${login}/plugins > music > querying lastfm api`)
const period = time_range === "short" ? "1month" : time_range === "medium" ? "6month" : "overall"
tracks =
top_type === "artists"
? (
await imports.axios.get(
`https://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=${user}&api_key=${token}&limit=${limit}&period=${period}&format=json`,
{
headers: {
"User-Agent": "lowlighter/metrics",
Accept: "application/json",
},
}
)
).data.topartists.artist.map(artist => ({
name: artist.name,
artist: `Play count: ${artist.playcount}`,
artwork: artist.image.reverse()[0]["#text"],
}))
: (
await imports.axios.get(
`https://ws.audioscrobbler.com/2.0/?method=user.gettoptracks&user=${user}&api_key=${token}&limit=${limit}&period=${period}&format=json`,
{
headers: {
"User-Agent": "lowlighter/metrics",
Accept: "application/json",
},
}
)
).data.toptracks.track.map(track => ({
name: track.name,
artist: track.artist.name,
artwork: track.image.reverse()[0]["#text"],
}))
tracks = top_type === "artists"
? (
await imports.axios.get(
`https://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=${user}&api_key=${token}&limit=${limit}&period=${period}&format=json`,
{
headers:{
"User-Agent":"lowlighter/metrics",
Accept:"application/json",
},
},
)
).data.topartists.artist.map(artist => ({
name:artist.name,
artist:`Play count: ${artist.playcount}`,
artwork:artist.image.reverse()[0]["#text"],
}))
: (
await imports.axios.get(
`https://ws.audioscrobbler.com/2.0/?method=user.gettoptracks&user=${user}&api_key=${token}&limit=${limit}&period=${period}&format=json`,
{
headers:{
"User-Agent":"lowlighter/metrics",
Accept:"application/json",
},
},
)
).data.toptracks.track.map(track => ({
name:track.name,
artist:track.artist.name,
artwork:track.image.reverse()[0]["#text"],
}))
}
//Handle errors
catch (error) {
@@ -526,4 +522,4 @@ function get_all_with_key(obj, key) {
result.push(...get_all_with_key(obj[i], key))
}
return result
}
}

View File

@@ -55,11 +55,11 @@ export default async function({login, q, imports, rest, graphql, data, account,
//Save user data
contribution.user = {
commits,
percentage:commits/contribution.history,
percentage:commits / contribution.history,
maintainer:maintainers.includes(login),
get stars() {
return this.maintainer ? stars : this.percentage*stars
}
return this.maintainer ? stars : this.percentage * stars
},
}
console.debug(`metrics/compute/${login}/plugins > notable > indepth > successfully processed ${owner}/${repo}`)
}
@@ -91,6 +91,7 @@ export default async function({login, q, imports, rest, graphql, data, account,
}
else
aggregated.set(key, {name:key, handle, avatar, organization, stars, aggregated:1, ..._extras})
}
contributions = [...aggregated.values()]
if (extras) {
@@ -100,7 +101,6 @@ export default async function({login, q, imports, rest, graphql, data, account,
contributions = contributions.sort((a, b) => ((b.user?.percentage + b.user?.maintainer) || 0) - ((a.user?.percentage + a.user?.maintainer) || 0))
}
//Results
return {contributions}
}

View File

@@ -3,10 +3,11 @@ export default async function({q, imports, data, account}, {enabled = false, tok
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
if ((!enabled)||(!q.poopmap))
if ((!enabled) || (!q.poopmap))
return null
if (!token) return {poops:[], days:7}
if (!token)
return {poops:[], days:7}
const {days} = imports.metadata.plugins.poopmap.inputs({data, account, q})
const {data:{poops}} = await imports.axios.get(`https://api.poopmap.net/api/v1/public_links/${token}`)
@@ -33,4 +34,4 @@ export default async function({q, imports, data, account}, {enabled = false, tok
catch (error) {
throw {error:{message:"An error occured", instance:error}}
}
}
}

View File

@@ -3,7 +3,7 @@ export default async function({login, q, imports, graphql, queries, data, accoun
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
if ((!enabled)||(!q.repositories))
if ((!enabled) || (!q.repositories))
return null
//Load inputs
@@ -35,4 +35,4 @@ export default async function({login, q, imports, graphql, queries, data, accoun
catch (error) {
throw {error:{message:"An error occured", instance:error}}
}
}
}

View File

@@ -3,7 +3,7 @@ export default async function({login, q, imports, data, graphql, queries, accoun
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
if ((!enabled)||(!q.sponsors))
if ((!enabled) || (!q.sponsors))
return null
//Load inputs

View File

@@ -31,7 +31,7 @@ export default async function({login, graphql, data, imports, q, queries, accoun
console.debug(`metrics/compute/${login}/plugins > stargazers > loaded ${dates.length} stargazers in total`)
//Compute stargazers increments
const days = 14 * (1 + data.large/2)
const days = 14 * (1 + data.large / 2)
const increments = {dates:Object.fromEntries([...new Array(days).fill(null).map((_, i) => [new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString().slice(0, 10), 0]).reverse()]), max:NaN, min:NaN}
dates
.map(date => date.toISOString().slice(0, 10))

View File

@@ -3,7 +3,7 @@ export default async function({login, q, imports, data, account}, {enabled = fal
//Plugin execution
try {
//Check if plugin is enabled and requirements are met
if ((!enabled)||(!q.starlists))
if ((!enabled) || (!q.starlists))
return null
//Load inputs
@@ -19,17 +19,18 @@ export default async function({login, q, imports, data, account}, {enabled = fal
console.debug(`metrics/compute/${login}/plugins > starlists > fetching lists`)
await page.goto(`https://github.com/${login}?tab=stars`)
let lists = (await page.evaluate(() => [...document.querySelectorAll("[href^='/stars/lowlighter/lists']")].map(element => ({
link:element.href,
name:element.querySelector("h3")?.innerText ?? "",
description:element.querySelector("span")?.innerText ?? "",
count:Number(element.querySelector("div")?.innerText.match(/(?<count>\d+)/)?.groups.count),
repositories:[]
}))))
link:element.href,
name:element.querySelector("h3")?.innerText ?? "",
description:element.querySelector("span")?.innerText ?? "",
count:Number(element.querySelector("div")?.innerText.match(/(?<count>\d+)/)?.groups.count),
repositories:[],
}))
))
const count = lists.length
console.debug(`metrics/compute/${login}/plugins > starlists > found [${lists.map(({name}) => name)}]`)
lists = lists
.filter(({name}) => name)
.filter(({name}) => (!only.length)||(only.includes(name.toLocaleLowerCase())))
.filter(({name}) => (!only.length) || (only.includes(name.toLocaleLowerCase())))
.filter(({name}) => !ignored.includes(name.toLocaleLowerCase()))
.slice(0, limit)
console.debug(`metrics/compute/${login}/plugins > starlists > extracted ${lists.length} lists`)
@@ -39,9 +40,10 @@ export default async function({login, q, imports, data, account}, {enabled = fal
console.debug(`metrics/compute/${login}/plugins > starlists > fetching ${list.name}`)
await page.goto(list.link)
const repositories = await page.evaluate(() => [...document.querySelectorAll("#user-list-repositories > div")].map(element => ({
name:element.querySelector("div:first-child")?.innerText.replace(" / ", "/") ?? "",
description:element.querySelector(".py-1")?.innerText ?? ""
})))
name:element.querySelector("div:first-child")?.innerText.replace(" / ", "/") ?? "",
description:element.querySelector(".py-1")?.innerText ?? "",
}))
)
list.repositories.push(...repositories)
if (_shuffle)
list.repositories = imports.shuffle(list.repositories)
@@ -59,4 +61,4 @@ export default async function({login, q, imports, data, account}, {enabled = fal
catch (error) {
throw {error:{message:"An error occured", instance:error}}
}
}
}

View File

@@ -16,7 +16,8 @@ export default async function({login, imports, data, rest, q, account}, {enabled
//Get views stats from repositories
console.debug(`metrics/compute/${login}/plugins > traffic > querying api`)
const views = {count:0, uniques:0}
const response = [...await Promise.allSettled(repositories.map(({repo, owner}) => (skipped.includes(repo.toLocaleLowerCase())) || (skipped.includes(`${owner}/${repo}`.toLocaleLowerCase())) ? {} : rest.repos.getViews({owner, repo})))].filter(({status}) => status === "fulfilled").map(({value}) => value)
const response = [...await Promise.allSettled(repositories.map(({repo, owner}) => (skipped.includes(repo.toLocaleLowerCase())) || (skipped.includes(`${owner}/${repo}`.toLocaleLowerCase())) ? {} : rest.repos.getViews({owner, repo})))].filter(({status}) => status === "fulfilled"
).map(({value}) => value)
//Compute views
console.debug(`metrics/compute/${login}/plugins > traffic > computing stats`)