From bd6a264c2e1320e93ce1afbbcd74d91602c234a3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 17:32:23 -0400 Subject: [PATCH 1/3] chore(deps): bump express-rate-limit from 6.5.2 to 6.6.0 (#1214) [skip ci] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7d78e09..336c1766 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "ejs": "^3.1.8", "emoji-name-map": "^1.2.9", "express": "^4.18.1", - "express-rate-limit": "^6.5.2", + "express-rate-limit": "^6.6.0", "file-type": "^18.0.0", "js-yaml": "^4.1.0", "linguist-js": "^2.5.2", @@ -5095,9 +5095,9 @@ } }, "node_modules/express-rate-limit": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.5.2.tgz", - "integrity": "sha512-N0cG/5ccbXfNC+FxRu7ujm2HjKkygF2PL7KLAf/hct9uqKB5QkZVizb/hEst6tUBXnfhblYWgOorN2eY+Saerw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz", + "integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==", "engines": { "node": ">= 12.9.0" }, @@ -13976,9 +13976,9 @@ } }, "express-rate-limit": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.5.2.tgz", - "integrity": "sha512-N0cG/5ccbXfNC+FxRu7ujm2HjKkygF2PL7KLAf/hct9uqKB5QkZVizb/hEst6tUBXnfhblYWgOorN2eY+Saerw==", + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.6.0.tgz", + "integrity": "sha512-HFN2+4ZGdkQOS8Qli4z6knmJFnw6lZed67o6b7RGplWeb1Z0s8VXaj3dUgPIdm9hrhZXTRpCTHXA0/2Eqex0vA==", "requires": {} }, "extend": { diff --git a/package.json b/package.json index 231e598d..43383734 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "ejs": "^3.1.8", "emoji-name-map": "^1.2.9", "express": "^4.18.1", - "express-rate-limit": "^6.5.2", + "express-rate-limit": "^6.6.0", "file-type": "^18.0.0", "js-yaml": "^4.1.0", "linguist-js": "^2.5.2", From 5747703b94d661caacb9e5f8fafe3a6b5b38c45f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Sep 2022 21:08:06 -0400 Subject: [PATCH 2/3] chore(deps): bump sharp from 0.30.7 to 0.31.0 (#1210) [skip ci] --- package-lock.json | 16 ++++++++-------- package.json | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 336c1766..563d8e17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "purgecss": "^4.1.3", "rss-parser": "^3.12.0", "sanitize-html": "^2.7.1", - "sharp": "^0.30.7", + "sharp": "^0.31.0", "simple-git": "^3.14.0", "svgo": "^2.8.0", "twemoji-parser": "^14.0.0", @@ -8721,9 +8721,9 @@ "optional": true }, "node_modules/sharp": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz", - "integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.0.tgz", + "integrity": "sha512-ft96f8WzGxavg0rkLpMw90MTPMUZDyf0tHjPPh8Ob59xt6KzX8EqtotcqZGUm7kwqpX2pmYiyYX2LL0IZ/FDEw==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", @@ -8736,7 +8736,7 @@ "tunnel-agent": "^0.6.0" }, "engines": { - "node": ">=12.13.0" + "node": ">=14.15.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -16687,9 +16687,9 @@ "optional": true }, "sharp": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.30.7.tgz", - "integrity": "sha512-G+MY2YW33jgflKPTXXptVO28HvNOo9G3j0MybYAHeEmby+QuD2U98dT6ueht9cv/XDqZspSpIhoSW+BAKJ7Hig==", + "version": "0.31.0", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.31.0.tgz", + "integrity": "sha512-ft96f8WzGxavg0rkLpMw90MTPMUZDyf0tHjPPh8Ob59xt6KzX8EqtotcqZGUm7kwqpX2pmYiyYX2LL0IZ/FDEw==", "requires": { "color": "^4.2.3", "detect-libc": "^2.0.1", diff --git a/package.json b/package.json index 43383734..32ef3b96 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "purgecss": "^4.1.3", "rss-parser": "^3.12.0", "sanitize-html": "^2.7.1", - "sharp": "^0.30.7", + "sharp": "^0.31.0", "simple-git": "^3.14.0", "svgo": "^2.8.0", "twemoji-parser": "^14.0.0", From 2cf5a3990a42bfbdd753fa6fef4491b7c2e12eba Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Mon, 5 Sep 2022 21:18:20 -0400 Subject: [PATCH 3/3] feat(plugins/leetcode): add plugin (#1213) [skip ci] --- .github/actions/spelling/expect.txt | 2 + .../app/web/statics/embed/app.placeholder.js | 28 +++++ source/plugins/leetcode/README.md | 12 +++ source/plugins/leetcode/examples.yml | 8 ++ source/plugins/leetcode/index.mjs | 54 ++++++++++ source/plugins/leetcode/metadata.yml | 59 ++++++++++ .../leetcode/queries/languages.graphql | 8 ++ .../plugins/leetcode/queries/problems.graphql | 18 ++++ .../plugins/leetcode/queries/recent.graphql | 8 ++ .../plugins/leetcode/queries/skills.graphql | 18 ++++ source/templates/classic/partials/_.json | 1 + .../templates/classic/partials/leetcode.ejs | 68 ++++++++++++ source/templates/classic/style.css | 40 +++++++ tests/mocks/api/axios/post/leetcode.mjs | 102 ++++++++++++++++++ 14 files changed, 426 insertions(+) create mode 100644 source/plugins/leetcode/README.md create mode 100644 source/plugins/leetcode/examples.yml create mode 100644 source/plugins/leetcode/index.mjs create mode 100644 source/plugins/leetcode/metadata.yml create mode 100644 source/plugins/leetcode/queries/languages.graphql create mode 100644 source/plugins/leetcode/queries/problems.graphql create mode 100644 source/plugins/leetcode/queries/recent.graphql create mode 100644 source/plugins/leetcode/queries/skills.graphql create mode 100644 source/templates/classic/partials/leetcode.ejs create mode 100644 tests/mocks/api/axios/post/leetcode.mjs diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 56ef12c8..ea248279 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -165,6 +165,8 @@ lastname leaderboard lecoq legoandmars +leetcode +LeetCode libgconf libssl libx diff --git a/source/app/web/statics/embed/app.placeholder.js b/source/app/web/statics/embed/app.placeholder.js index b1105fdf..c399ed4a 100644 --- a/source/app/web/statics/embed/app.placeholder.js +++ b/source/app/web/statics/embed/app.placeholder.js @@ -1015,6 +1015,34 @@ }, }) : null), + //LeetCode + ...(set.plugins.enabled.leetcode + ? ({ + leetcode: { + user: options["leetcode.user"], + sections: options["leetcode.sections"].split(",").map(x => x.trim()).filter(x => x), + languages: new Array(6).fill(null).map(_ => ({ + language:faker.hacker.noun(), + solved:faker.datatype.number(200) + })), + skills: new Array(Number(options["leetcode.limit.skills"]) || 10).fill(null).map(_ => ({ + name:faker.hacker.noun(), + category:faker.helpers.arrayElement(["advanced", "intermediate", "fundamental"]), + solved:faker.datatype.number(30) + })), + problems: { + All: { count: 2402, solved: faker.datatype.number(2402) }, + Easy: { count: 592, solved: faker.datatype.number(592) }, + Medium: { count: 1283, solved: faker.datatype.number(1283) }, + Hard: { count: 527, solved: faker.datatype.number(527) } + }, + recent: new Array(Number(options["leetcode.limit.recent"]) || 2).fill(null).map(_ => ({ + title:faker.lorem.sentence(), + date:faker.date.recent(), + })), + }, + }) + : null), //Activity ...(set.plugins.enabled.activity ? ({ diff --git a/source/plugins/leetcode/README.md b/source/plugins/leetcode/README.md new file mode 100644 index 00000000..3eff2a87 --- /dev/null +++ b/source/plugins/leetcode/README.md @@ -0,0 +1,12 @@ + + + +## ➡️ Available options + + + + +## ℹ️ Examples workflows + + + diff --git a/source/plugins/leetcode/examples.yml b/source/plugins/leetcode/examples.yml new file mode 100644 index 00000000..539b8a51 --- /dev/null +++ b/source/plugins/leetcode/examples.yml @@ -0,0 +1,8 @@ +- name: LeetCode + uses: lowlighter/metrics@latest + with: + filename: metrics.plugin.leetcode.svg + token: NOT_NEEDED + base: "" + plugin_leetcode: yes + plugin_leetcode_sections: solved, skills, recent diff --git a/source/plugins/leetcode/index.mjs b/source/plugins/leetcode/index.mjs new file mode 100644 index 00000000..4c120c13 --- /dev/null +++ b/source/plugins/leetcode/index.mjs @@ -0,0 +1,54 @@ +//Setup +export default async function({login, q, imports, data, queries, account}, {enabled = false, extras = false} = {}) { + //Plugin execution + try { + //Check if plugin is enabled and requirements are met + if ((!q.leetcode) || (!imports.metadata.plugins.leetcode.enabled(enabled, {extras}))) + return null + + //Load inputs + let {user, sections, "limit.skills":_limit_skills, "limit.recent":_limit_recent} = imports.metadata.plugins.leetcode.inputs({data, account, q}) + const result = {user, sections, languages:[], skills:[], problems:{}, recent:[]} + + //Languages stats + { + console.debug(`metrics/compute/${login}/plugins > leetcode > querying api (languages statistics)`) + const {data:{data:{matchedUser:{languageProblemCount:languages}}}} = await imports.axios.post("https://leetcode.com/graphql", {variables: {username: user}, query: queries.leetcode.languages()}) + result.languages = languages.map(({languageName:language, problemsSolved:solved}) => ({language, solved})) + } + + //Skills stats + { + console.debug(`metrics/compute/${login}/plugins > leetcode > querying api (skills statistics)`) + const {data:{data:{matchedUser:{tagProblemCounts:skills}}}} = await imports.axios.post("https://leetcode.com/graphql", {variables: {username: user}, query: queries.leetcode.skills()}) + for (const category in skills) + result.skills.push(...skills[category].map(({tagName:name, problemsSolved:solved}) => ({name, solved, category}))) + result.skills.sort((a, b) => b.solved - a.solved) + result.skills = result.skills.slice(0, _limit_skills || Infinity) + } + + //Problems + { + console.debug(`metrics/compute/${login}/plugins > leetcode > querying api (problems statistics)`) + const {data:{data:{allQuestionsCount:all, matchedUser:{submitStatsGlobal:{acSubmissionNum:submissions}}}}} = await imports.axios.post("https://leetcode.com/graphql", {variables: {username: user}, query: queries.leetcode.problems()}) + for (const {difficulty, count} of all) + result.problems[difficulty] = {count, solved:0} + for (const {difficulty, count:solved} of submissions) + result.problems[difficulty].solved = solved + } + + //Recent submissions + { + console.debug(`metrics/compute/${login}/plugins > leetcode > querying api (recent submissions statistics)`) + const {data:{data:{recentAcSubmissionList:submissions}}} = await imports.axios.post("https://leetcode.com/graphql", {variables: {username: user, limit:_limit_recent}, query: queries.leetcode.recent()}) + result.recent = submissions.map(({title, timestamp}) => ({title, date:new Date(timestamp*1000)})) + } + + //Results + return result + } + //Handle errors + catch (error) { + throw imports.format.error(error) + } +} \ No newline at end of file diff --git a/source/plugins/leetcode/metadata.yml b/source/plugins/leetcode/metadata.yml new file mode 100644 index 00000000..452d0e15 --- /dev/null +++ b/source/plugins/leetcode/metadata.yml @@ -0,0 +1,59 @@ +name: 🗳️ Leetcode +category: social +description: | + This plugin displays statistics from a [LeetCode](https://leetcode.com) account. +disclaimer: | + This plugin is not affiliated, associated, authorized, endorsed by, or in any way officially connected with [LeetCode](https://leetcode.com). + All product and company names are trademarks™ or registered® trademarks of their respective holders. +examples: + default: https://github.com/lowlighter/metrics/blob/examples/metrics.plugin.leetcode.svg +index: 9 +supports: + - user +scopes: [] +inputs: + + plugin_leetcode: + description: | + Enable leetcode plugin + type: boolean + default: no + + plugin_leetcode_user: + type: string + description: | + LeetCode login + default: .user.login + preset: no + + plugin_leetcode_sections: + description: | + Displayed sections + + - `solved` will display solved problems scores + - `skills` will display solved problems tagged skills + - `recent` will display recent submissions + type: array + format: comma-separated + default: solved + example: solved, skills, recent + values: + - solved + - skills + - recent + + plugin_leetcode_limit_skills: + description: | + Display limit (skills) + type: number + default: 10 + min: 0 + zero: disable + + plugin_leetcode_limit_recent: + description: | + Display limit (recent) + type: number + default: 2 + min: 1 + max: 15 \ No newline at end of file diff --git a/source/plugins/leetcode/queries/languages.graphql b/source/plugins/leetcode/queries/languages.graphql new file mode 100644 index 00000000..bd601f9e --- /dev/null +++ b/source/plugins/leetcode/queries/languages.graphql @@ -0,0 +1,8 @@ +query Languages ($username: String!) { + matchedUser(username: $username) { + languageProblemCount { + languageName + problemsSolved + } + } +} \ No newline at end of file diff --git a/source/plugins/leetcode/queries/problems.graphql b/source/plugins/leetcode/queries/problems.graphql new file mode 100644 index 00000000..09681f2e --- /dev/null +++ b/source/plugins/leetcode/queries/problems.graphql @@ -0,0 +1,18 @@ +query Problems ($username: String!) { + allQuestionsCount { + difficulty + count + } + matchedUser(username: $username) { + problemsSolvedBeatsStats { + difficulty + percentage + } + submitStatsGlobal { + acSubmissionNum { + difficulty + count + } + } + } +} \ No newline at end of file diff --git a/source/plugins/leetcode/queries/recent.graphql b/source/plugins/leetcode/queries/recent.graphql new file mode 100644 index 00000000..9277b7d8 --- /dev/null +++ b/source/plugins/leetcode/queries/recent.graphql @@ -0,0 +1,8 @@ +query Recent ($username: String!, $limit: Int!) { + recentAcSubmissionList(username: $username, limit: $limit) { + id + title + titleSlug + timestamp + } +} \ No newline at end of file diff --git a/source/plugins/leetcode/queries/skills.graphql b/source/plugins/leetcode/queries/skills.graphql new file mode 100644 index 00000000..ec53c6dd --- /dev/null +++ b/source/plugins/leetcode/queries/skills.graphql @@ -0,0 +1,18 @@ +query Skills ($username: String!) { + matchedUser(username: $username) { + tagProblemCounts { + advanced { + tagName + problemsSolved + } + intermediate { + tagName + problemsSolved + } + fundamental { + tagName + problemsSolved + } + } + } +} \ No newline at end of file diff --git a/source/templates/classic/partials/_.json b/source/templates/classic/partials/_.json index d6ee0cd2..c65a757e 100644 --- a/source/templates/classic/partials/_.json +++ b/source/templates/classic/partials/_.json @@ -32,6 +32,7 @@ "skyline", "support", "stackoverflow", + "leetcode", "stock", "achievements", "screenshot", diff --git a/source/templates/classic/partials/leetcode.ejs b/source/templates/classic/partials/leetcode.ejs new file mode 100644 index 00000000..cfc9f096 --- /dev/null +++ b/source/templates/classic/partials/leetcode.ejs @@ -0,0 +1,68 @@ +<% if (plugins.leetcode) { %> +
+

+ + LeetCode statistics <% if (plugins.leetcode?.user) { %>for <%= plugins.leetcode.user %><% } %> +

+ <% if (plugins.leetcode.error) { %> +
+
+
+ + <%= plugins.leetcode.error.message %> +
+
+
+ <% } else { %> + <% if (plugins.leetcode.sections.includes("solved")) { %> +
+
+
+ <% for (const difficulty of ["All", "Easy", "Medium", "Hard"]) { const problems = plugins.leetcode.problems[difficulty], width = 440 * (1 + large) %> +
+ + + + <%= problems.solved %> + /<%= problems.count %> + + <%= difficulty %> +
+ <% } %> +
+
+
+ <% } %> + <% if (plugins.leetcode.sections.includes("skills")) { %> +
+

+ + Skills +

+
+ <% for (const {name, solved, category} of plugins.leetcode.skills) { %> +
<%= name %> x<%= solved%>
+ <% } %> +
+
+ <% } %> + <% if (plugins.leetcode.sections.includes("recent")) { %> +
+

+ + Recent submissions +

+ <% for (const {title, date} of plugins.leetcode.recent) { %> +
+ +
+
<%= title %>
+
<%= f.date(new Date(date), {date:true, timeZone:config.timezone?.name}) %>
+
+
+ <% } %> +
+ <% } %> + <% } %> +
+<% } %> diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index 57ef1ff4..00e5b7b9 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -429,6 +429,12 @@ text-anchor: middle; font-weight: 600; } + .gauge text.secondary { + fill: currentColor; + font-size: 25px; + font-family: monospace; + text-anchor: middle; + } .gauge .title { font-size: 18px; color: #777777; @@ -1336,6 +1342,40 @@ overflow: hidden; } +/* LeetCode */ + .leetcode.subsection { + padding-left: 28px; + } + .leetcode .topics { + margin-left: 20px; + } + .leetcode .count { + font-size: 10px; + color: #666666; + } + .leetcode .fundamental .dot, .leetcode .easy.gauge .gauge-arc { + color: #2CBB5D; + } + .leetcode .intermediate .dot, .leetcode .medium.gauge .gauge-arc { + color: #FFC01E; + } + .leetcode .advanced .dot, .leetcode .hard.gauge .gauge-arc { + color: #EF4743; + } + .leetcode .all.gauge .gauge-arc { + color: rgb(255, 161, 22); + } + .leetcode { + align-items: flex-start; + } + .leetcode .infos { + margin-bottom: 3px; + } + .leetcode .infos .date { + font-size: 10px; + color: #666666; + } + /* Code snippet */ .snippet .body { padding-left: 12px; diff --git a/tests/mocks/api/axios/post/leetcode.mjs b/tests/mocks/api/axios/post/leetcode.mjs new file mode 100644 index 00000000..f39e14f0 --- /dev/null +++ b/tests/mocks/api/axios/post/leetcode.mjs @@ -0,0 +1,102 @@ +/**Mocked data */ +export default function({faker, url, body, login = faker.internet.userName()}) { + if (/^https:..leetcode.com.graphql.*$/.test(url)) { + const {query} = body + //Languages query + if (/^query Languages /.test(query)) { + console.debug("metrics/compute/mocks > mocking leetcode api result > Languages") + return ({ + status: 200, + data: { + data: { + matchedUser:{ + languageProblemCount:new Array(6).fill(null).map(_ => ({ + languageName:faker.hacker.noun(), + problemsSolved:faker.datatype.number(200) + })) + } + }, + }, + }) + } + //Skills query + if (/^query Skills /.test(query)) { + console.debug("metrics/compute/mocks > mocking leetcode api result > Skills") + return ({ + status: 200, + data: { + data: { + matchedUser:{ + tagProblemCounts:{ + advanced:new Array(6).fill(null).map(_ => ({ + tagName:faker.hacker.noun(), + tagSlug:faker.lorem.slug(), + problemsSolved:faker.datatype.number(200) + })), + intermediate:new Array(6).fill(null).map(_ => ({ + tagName:faker.hacker.noun(), + tagSlug:faker.lorem.slug(), + problemsSolved:faker.datatype.number(200) + })), + fundamental:new Array(6).fill(null).map(_ => ({ + tagName:faker.hacker.noun(), + tagSlug:faker.lorem.slug(), + problemsSolved:faker.datatype.number(200) + })) + } + } + }, + }, + }) + } + //Problems query + if (/^query Problems /.test(query)) { + console.debug("metrics/compute/mocks > mocking leetcode api result > Problems") + return ({ + status: 200, + data: { + data: { + allQuestionsCount:[ + {difficulty:"All", count:2402}, + {difficulty:"Easy", count:592}, + {difficulty:"Medium", count:1283}, + {difficulty:"Hard", count:527}, + ], + matchedUser:{ + problemsSolvedBeatsStats:[ + {difficulty:"Easy", percentage:faker.datatype.float({max:100})}, + {difficulty:"Medium", percentage:faker.datatype.float({max:100})}, + {difficulty:"Hard", percentage:faker.datatype.float({max:100})}, + ], + submitStatsGlobal:{ + acSubmissionNum:[ + {difficulty:"All", count:faker.datatype.number(2402)}, + {difficulty:"Easy", count:faker.datatype.number(592)}, + {difficulty:"Medium", count:faker.datatype.number(1283)}, + {difficulty:"Hard", count:faker.datatype.number(527)}, + ] + } + } + }, + }, + }) + } + //Recent query + if (/^query Recent /.test(query)) { + console.debug("metrics/compute/mocks > mocking leetcode api result > Recent") + return ({ + status: 200, + data: { + data: { + recentAcSubmissionList:new Array(6).fill(null).map(_ => ({ + id:`${faker.datatype.number(10000)}`, + title:faker.lorem.sentence(), + titleSlug:faker.lorem.slug(), + timestamp:`${Math.round(faker.date.recent().getTime()/1000)}`, + })), + }, + }, + }) + } + } +}