From 7f3a1b365e42179adb49042cd92dd0042d3cfa8e Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Thu, 8 Apr 2021 20:13:47 +0200 Subject: [PATCH] Feat plugin improves (#214) --- source/.eslintrc.yml | 1 - source/app/action/index.mjs | 2 +- source/app/metrics/utils.mjs | 6 +- .../github/graphql/achievements.ranking.mjs | 2 + source/app/web/instance.mjs | 2 +- source/plugins/achievements/index.mjs | 72 +++++++++++++++++-- .../achievements/queries/ranking.graphql | 4 +- .../classic/partials/achievements.ejs | 6 +- 8 files changed, 80 insertions(+), 15 deletions(-) diff --git a/source/.eslintrc.yml b/source/.eslintrc.yml index 07ddc07e..04f525a7 100644 --- a/source/.eslintrc.yml +++ b/source/.eslintrc.yml @@ -64,7 +64,6 @@ rules: # Code integrity no-unsafe-optional-chaining: error no-duplicate-imports: error - no-promise-executor-return: error eqeqeq: error # Code simplicity diff --git a/source/app/action/index.mjs b/source/app/action/index.mjs index f4931f43..01ae4de1 100644 --- a/source/app/action/index.mjs +++ b/source/app/action/index.mjs @@ -32,7 +32,7 @@ //Waiter async function wait(seconds) { - await new Promise(solve => setTimeout(solve, seconds*1000)) //eslint-disable-line no-promise-executor-return + await new Promise(solve => setTimeout(solve, seconds*1000)) } //Runner diff --git a/source/app/metrics/utils.mjs b/source/app/metrics/utils.mjs index 7df5fb37..12870f67 100644 --- a/source/app/metrics/utils.mjs +++ b/source/app/metrics/utils.mjs @@ -198,7 +198,7 @@ if (animated) document.querySelector("svg").classList.add("no-animations") console.debug(`animations are ${animated ? "enabled" : "disabled"}`) - await new Promise(solve => setTimeout(solve, 2400)) //eslint-disable-line no-promise-executor-return + 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}`) @@ -270,7 +270,7 @@ /**Wait */ export async function wait(seconds) { - await new Promise(solve => setTimeout(solve, seconds*1000)) //eslint-disable-line no-promise-executor-return + await new Promise(solve => setTimeout(solve, seconds*1000)) } /**Create record from puppeteer browser */ @@ -306,7 +306,7 @@ //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)))) //eslint-disable-line no-promise-executor-return + encoder.addFrame(await new Promise(solve => buffer.decode(pixels => solve(pixels)))) if (frames%10 === 0) console.debug(`metrics/puppeteergif > processed ${i}/${frames} frames`) } diff --git a/source/app/mocks/api/github/graphql/achievements.ranking.mjs b/source/app/mocks/api/github/graphql/achievements.ranking.mjs index 66d402b7..7a789e06 100644 --- a/source/app/mocks/api/github/graphql/achievements.ranking.mjs +++ b/source/app/mocks/api/github/graphql/achievements.ranking.mjs @@ -3,6 +3,8 @@ console.debug("metrics/compute/mocks > mocking graphql api result > achievements/ranking") return ({ repo_rank:{repositoryCount:faker.random.number(100000)}, + forks_rank:{repositoryCount:faker.random.number(100000)}, + created_rank:{userCount:faker.random.number(100000)}, user_rank:{userCount:faker.random.number(100000)}, repo_total:{repositoryCount:faker.random.number(100000)}, user_total:{userCount:faker.random.number(100000)}, diff --git a/source/app/web/instance.mjs b/source/app/web/instance.mjs index 90221ff9..a033a6d1 100644 --- a/source/app/web/instance.mjs +++ b/source/app/web/instance.mjs @@ -167,7 +167,7 @@ await pending.get(login) } else - pending.set(login, new Promise(_solve => solve = _solve)) //eslint-disable-line no-promise-executor-return + 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`) diff --git a/source/plugins/achievements/index.mjs b/source/plugins/achievements/index.mjs index 8c87f8ce..933c52dc 100644 --- a/source/plugins/achievements/index.mjs +++ b/source/plugins/achievements/index.mjs @@ -9,10 +9,13 @@ //Load inputs let {threshold, secrets, only, ignored, limit} = imports.metadata.plugins.achievements.inputs({data, q, account}) - //Initinalization + //Initialization const list = [] const {user} = await graphql(queries.achievements({login})) - const ranks = await graphql(queries.achievements.ranking({followers:user.followers.totalCount, stars:user.popular.nodes?.[0]?.stargazers?.totalCount ?? 0})) + 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} + await total({imports}) //Developer { @@ -23,6 +26,7 @@ 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"}), }) } @@ -152,7 +156,7 @@ text:`Followed by ${value} user${imports.s(value)}`, icon:"", ...rank(value, [1, 200, 500, 1000]), value, unlock:new Date(unlock?.createdAt), - gh:Number(`1${"0".repeat(Math.ceil(Math.log10(1+ranks.user_rank.userCount)))}`), + leaderboard:leaderboard({user:ranks.user_rank.userCount, requirement:scores.followers >= requirements.followers, type:"users"}), }) } @@ -166,7 +170,20 @@ text:`Maintaining a repository with ${value} star${imports.s(value)}`, icon:"", ...rank(value, [1, 1000, 5000, 10000]), value, unlock:new Date(unlock?.createdAt), - gh:Number(`1${"0".repeat(Math.ceil(Math.log10(1+ranks.repo_rank.repositoryCount)))}`), + 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"}), }) } @@ -289,4 +306,51 @@ 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, + 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) + }, + } : null + } + +/**Total extracter */ + async function total({imports}) { + if (!total.promise) { + total.promise = new Promise(async(solve, reject) => { //eslint-disable-line no-async-promise-executor + //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 diff --git a/source/plugins/achievements/queries/ranking.graphql b/source/plugins/achievements/queries/ranking.graphql index 4901fa86..5986d405 100644 --- a/source/plugins/achievements/queries/ranking.graphql +++ b/source/plugins/achievements/queries/ranking.graphql @@ -5,10 +5,10 @@ query AchievementsRanking { user_rank:search(query: "followers:>$followers", type: USER, first: 0) { userCount } - repo_total:search(query: "stars:>-1", type: REPOSITORY, first: 0) { + forks_rank:search(query: "forks:>$forks", type: REPOSITORY, first: 0) { repositoryCount } - user_total:search(query: "followers:>-1", type: USER, first: 0) { + created_rank:search(query: "repos:>$created", type: USER, first: 0) { userCount } } \ No newline at end of file diff --git a/source/templates/classic/partials/achievements.ejs b/source/templates/classic/partials/achievements.ejs index 0c251d68..ad35f968 100644 --- a/source/templates/classic/partials/achievements.ejs +++ b/source/templates/classic/partials/achievements.ejs @@ -12,7 +12,7 @@ <%= plugins.achievements.error.message %> <% } else { %> - <% for (const {title, text, icon, rank, gh = NaN, progress = 0, unlock = null} of plugins.achievements.list) { %> + <% for (const {title, text, icon, rank, leaderboard = null, progress = 0, unlock = null} of plugins.achievements.list) { %> "> @@ -33,9 +33,9 @@ <%= title %> - <% if ((Number.isFinite(gh))&&(gh < 100000)) { %> + <% if (leaderboard) { %> - Top <%= gh %> + ranked <%= f(leaderboard.user) %> out of <%= f(leaderboard.total) %> <%= leaderboard.type %> <% } %>