From 220deb05e311b5e3a2cba30afff097c263931929 Mon Sep 17 00:00:00 2001 From: Simon Lecoq <22963968+lowlighter@users.noreply.github.com> Date: Mon, 16 Jan 2023 22:28:33 -0500 Subject: [PATCH] feat(plugins/sponsorships): add plugin (#1358) --- .github/actions/spelling/allow.txt | 1 + .../app/web/statics/embed/app.placeholder.js | 20 ++++++++ source/plugins/sponsorships/README.md | 12 +++++ source/plugins/sponsorships/examples.yml | 7 +++ source/plugins/sponsorships/index.mjs | 51 +++++++++++++++++++ source/plugins/sponsorships/metadata.yml | 41 +++++++++++++++ .../plugins/sponsorships/queries/all.graphql | 29 +++++++++++ .../sponsorships/queries/sponsorships.graphql | 10 ++++ source/templates/classic/partials/_.json | 1 + .../classic/partials/sponsorships.ejs | 40 +++++++++++++++ source/templates/classic/style.css | 11 ++++ .../api/github/graphql/sponsorships.all.mjs | 33 ++++++++++++ .../github/graphql/sponsorships.default.mjs | 12 +++++ 13 files changed, 268 insertions(+) create mode 100644 source/plugins/sponsorships/README.md create mode 100644 source/plugins/sponsorships/examples.yml create mode 100644 source/plugins/sponsorships/index.mjs create mode 100644 source/plugins/sponsorships/metadata.yml create mode 100644 source/plugins/sponsorships/queries/all.graphql create mode 100644 source/plugins/sponsorships/queries/sponsorships.graphql create mode 100644 source/templates/classic/partials/sponsorships.ejs create mode 100644 tests/mocks/api/github/graphql/sponsorships.all.mjs create mode 100644 tests/mocks/api/github/graphql/sponsorships.default.mjs diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 32292b85..bd6e6acc 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -1,6 +1,7 @@ deno gpgarmor github +githubassets https leetcode pgn diff --git a/source/app/web/statics/embed/app.placeholder.js b/source/app/web/statics/embed/app.placeholder.js index f6534eb0..65aa3805 100644 --- a/source/app/web/statics/embed/app.placeholder.js +++ b/source/app/web/statics/embed/app.placeholder.js @@ -458,6 +458,26 @@ }, }) : null), + //Sponsorships + ...(set.plugins.enabled.sponsorships + ? ({ + sponsorships: { + sections: options["sponsorships.sections"].split(",").map(x => x.trim()), + amount: faker.datatype.number(1000), + list: new Array(2+faker.datatype.number(8)).fill(null).map(_ => ({ + login: faker.internet.userName(), + avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + type: "user", + tier: `$${faker.datatype.number(100) * 10} per month`, + private: false, + past: faker.datatype.boolean(), + })), + size: Number(options["sponsorships.size"]), + image: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==", + started: faker.date.recent(), + }, + }) + : null), //Languages ...(set.plugins.enabled.languages ? ({ diff --git a/source/plugins/sponsorships/README.md b/source/plugins/sponsorships/README.md new file mode 100644 index 00000000..3eff2a87 --- /dev/null +++ b/source/plugins/sponsorships/README.md @@ -0,0 +1,12 @@ + + + +## âžĄī¸ Available options + + + + +## â„šī¸ Examples workflows + + + diff --git a/source/plugins/sponsorships/examples.yml b/source/plugins/sponsorships/examples.yml new file mode 100644 index 00000000..d5a04ee6 --- /dev/null +++ b/source/plugins/sponsorships/examples.yml @@ -0,0 +1,7 @@ +- name: 💝 GitHub sponsorships + uses: lowlighter/metrics@latest + with: + filename: metrics.plugin.sponsorships.svg + token: ${{ secrets.METRICS_TOKEN }} + base: "" + plugin_sponsorships: yes diff --git a/source/plugins/sponsorships/index.mjs b/source/plugins/sponsorships/index.mjs new file mode 100644 index 00000000..946446da --- /dev/null +++ b/source/plugins/sponsorships/index.mjs @@ -0,0 +1,51 @@ +//Setup +export default async function({login, q, imports, data, graphql, queries, account}, {enabled = false, extras = false} = {}) { + //Plugin execution + try { + //Check if plugin is enabled and requirements are met + if ((!q.sponsorships) || (!imports.metadata.plugins.sponsorships.enabled(enabled, {extras}))) + return null + + //Load inputs + let {sections, size} = await imports.metadata.plugins.sponsorships.inputs({data, account, q}) + + //Query description and goal + let amount = NaN, image = null, started = null + if (sections.includes("amount")) { + console.debug(`metrics/compute/${login}/plugins > sponsorships > querying total amount spend`) + const {totalSponsorshipAmountAsSponsorInCents, sponsorshipsAsSponsor} = (await graphql(queries.sponsorships({login, account})))[account] + amount = totalSponsorshipAmountAsSponsorInCents/100 + image = "https://github.githubassets.com/images/icons/emoji/hearts_around.png" + started = sponsorshipsAsSponsor.nodes[0]?.createdAt ? new Date(sponsorshipsAsSponsor.nodes[0]?.createdAt) : null + } + image = await imports.imgb64(image) + + //Query sponsorships + const list = [] + if (sections.includes("sponsorships")) { + console.debug(`metrics/compute/${login}/plugins > sponsorships > querying sponsorships`) + { + const fetched = [] + let cursor = null + let pushed = 0 + do { + console.debug(`metrics/compute/${login}/sponsorships > retrieving sponsorships after ${cursor}`) + const {[account]: {sponsorshipsAsSponsor: {edges, nodes}}} = await graphql(queries.sponsorships.all({login, account, after: cursor ? `after: "${cursor}"` : "", size: Math.round(size * 1.5)})) + cursor = edges?.[edges?.length - 1]?.cursor + fetched.push(...nodes) + pushed = nodes.length + console.debug(`metrics/compute/${login}/sponsorships > retrieved ${pushed} sponsorships events after ${cursor}`) + } while ((pushed) && (cursor)) + list.push(...fetched.map(({sponsorable: {login, avatarUrl, url: organization = null}, tier:{name:tier}, privacyLevel:privacy, isActive:active}) => ({login, avatarUrl, type: organization ? "organization" : "user", tier, private: privacy !== "PUBLIC", past:!active}))) + } + await Promise.all(list.map(async user => user.avatar = await imports.imgb64(user.avatarUrl))) + } + + //Results + return {amount, list, sections, size, image, started} + } + //Handle errors + catch (error) { + throw imports.format.error(error) + } +} \ No newline at end of file diff --git a/source/plugins/sponsorships/metadata.yml b/source/plugins/sponsorships/metadata.yml new file mode 100644 index 00000000..2ccd6d2c --- /dev/null +++ b/source/plugins/sponsorships/metadata.yml @@ -0,0 +1,41 @@ +name: 💝 GitHub Sponsorships +category: github +description: | + This plugin displays sponsorships funded through [GitHub sponsors](https://github.com/sponsors/). +examples: + default: https://github.com/lowlighter/metrics/blob/examples/metrics.plugin.sponsorships.svg +supports: + - user + - organization +scopes: + - read:user + - read:org +inputs: + + plugin_sponsorships: + description: | + Enable sponsorships plugin + type: boolean + default: no + + plugin_sponsorships_sections: + description: | + Displayed sections + + - `amount`: display total amount sponsored + - `sponsorships`: display GitHub sponsorships + type: array + format: comma-separated + default: amount, sponsorships + example: amount, sponsorships + values: + - amount + - sponsorships + + plugin_sponsorships_size: + description: | + Profile picture display size + type: number + default: 24 + min: 8 + max: 64 \ No newline at end of file diff --git a/source/plugins/sponsorships/queries/all.graphql b/source/plugins/sponsorships/queries/all.graphql new file mode 100644 index 00000000..5468b516 --- /dev/null +++ b/source/plugins/sponsorships/queries/all.graphql @@ -0,0 +1,29 @@ +query SponsorshipsAll { + $account(login: "$login") { + sponsorshipsAsSponsor($after first: 100, activeOnly: false, orderBy: {field: CREATED_AT, direction: DESC}) { + edges { + cursor + } + nodes { + createdAt + isActive + isOneTimePayment + tier { + name + } + privacyLevel + sponsorable { + ... on User { + avatarUrl(size: $size) + login + } + ... on Organization { + login + avatarUrl(size: $size) + url + } + } + } + } + } +} diff --git a/source/plugins/sponsorships/queries/sponsorships.graphql b/source/plugins/sponsorships/queries/sponsorships.graphql new file mode 100644 index 00000000..d593bae1 --- /dev/null +++ b/source/plugins/sponsorships/queries/sponsorships.graphql @@ -0,0 +1,10 @@ +query SponsorshipsDefault { + $account(login: "$login") { + totalSponsorshipAmountAsSponsorInCents + sponsorshipsAsSponsor(first: 1, activeOnly: false, orderBy: {field: CREATED_AT, direction: ASC}) { + nodes { + createdAt + } + } + } +} \ No newline at end of file diff --git a/source/templates/classic/partials/_.json b/source/templates/classic/partials/_.json index 2a71867b..e8523ae5 100644 --- a/source/templates/classic/partials/_.json +++ b/source/templates/classic/partials/_.json @@ -39,6 +39,7 @@ "code", "chess", "sponsors", + "sponsorships", "poopmap", "fortune", "splatoon" diff --git a/source/templates/classic/partials/sponsorships.ejs b/source/templates/classic/partials/sponsorships.ejs new file mode 100644 index 00000000..3ccca264 --- /dev/null +++ b/source/templates/classic/partials/sponsorships.ejs @@ -0,0 +1,40 @@ +<% if (plugins.sponsorships) { %> +
+

+ + Sponsorships +

+ <% if (plugins.sponsorships.error) { %> +
+
+
+ + <%= plugins.sponsorships.error.message %> +
+
+
+ <% } else { %> + <% for (const section of plugins.sponsorships.sections) { %> +
+ <% if (section === "amount") { %> +
+ +
<%= user.login %> has given a total of <%= new Intl.NumberFormat("en", {style:"currency", currency:"USD"}).format(plugins.sponsorships.amount) %> to open source software<% if (plugins.sponsorships.started) { %> since <%= f.date(plugins.sponsorships.started, { date:true, timeZone:config.timezone?.name}) %><% } %>.
+
+ <% } else if (section === "sponsorships") { %> +
+
+ + <%= user.login %> helped funding the work of <%= plugins.sponsorships.list.length %> users and organizations. + +
+
+ <% for (const user of plugins.sponsorships.list) { %>" src="<%= user.avatar %>" width="<%= 0.8*plugins.sponsorships.size %>" height="<%= 0.8*plugins.sponsorships.size %>" alt="" /><% } %> +
+
+ <% } %> +
+ <% } %> + <% } %> +
+<% } %> diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css index f7788019..a9abb963 100644 --- a/source/templates/classic/style.css +++ b/source/templates/classic/style.css @@ -1161,6 +1161,17 @@ .sponsors .avatar { margin: 2px; } + .sponsorships.goal { + display: flex; + align-items: center; + } + .sponsorships.goal .bold { + font-weight: bold; + } + .sponsorships.goal img { + width: 38px; + margin-right: 8px; + } /* Stackoverflow */ .stackoverflow { diff --git a/tests/mocks/api/github/graphql/sponsorships.all.mjs b/tests/mocks/api/github/graphql/sponsorships.all.mjs new file mode 100644 index 00000000..6823e5b2 --- /dev/null +++ b/tests/mocks/api/github/graphql/sponsorships.all.mjs @@ -0,0 +1,33 @@ +/**Mocked data */ +export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > sponsorships/all") + return /after: "MOCKED_CURSOR"/m.test(query) + ? ({ + user: { + sponsorshipsAsSponsor: { + edges: [], + nodes: [], + }, + }, + }) + : ({ + user: { + sponsorshipsAsSponsor: { + edges: new Array(10).fill("MOCKED_CURSOR"), + nodes: new Array(10).fill(null).map(_ => ({ + createdAt: `${faker.date.recent()}`, + isActive: faker.datatype.boolean(), + isOneTimePayment: faker.datatype.boolean(), + tier: { + name: "$X a month" + }, + privacyLevel: "PUBLIC", + sponsorable: { + login: faker.internet.userName(), + avatarUrl: null, + }, + })), + }, + }, + }) +} diff --git a/tests/mocks/api/github/graphql/sponsorships.default.mjs b/tests/mocks/api/github/graphql/sponsorships.default.mjs new file mode 100644 index 00000000..98040569 --- /dev/null +++ b/tests/mocks/api/github/graphql/sponsorships.default.mjs @@ -0,0 +1,12 @@ +/**Mocked data */ +export default function({faker, query, login = faker.internet.userName()}) { + console.debug("metrics/compute/mocks > mocking graphql api result > sponsorships/default") + return ({ + user: { + totalSponsorshipAmountAsSponsorInCents:faker.datatype.number(100000), + sponsorshipsAsSponsor:{ + nodes:[{createdAt: `${faker.date.recent()}`}] + } + }, + }) +}