feat(plugins/sponsorships): add plugin (#1358)
This commit is contained in:
1
.github/actions/spelling/allow.txt
vendored
1
.github/actions/spelling/allow.txt
vendored
@@ -1,6 +1,7 @@
|
||||
deno
|
||||
gpgarmor
|
||||
github
|
||||
githubassets
|
||||
https
|
||||
leetcode
|
||||
pgn
|
||||
|
||||
@@ -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
|
||||
? ({
|
||||
|
||||
12
source/plugins/sponsorships/README.md
Normal file
12
source/plugins/sponsorships/README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
<!--header-->
|
||||
<!--/header-->
|
||||
|
||||
## ➡️ Available options
|
||||
|
||||
<!--options-->
|
||||
<!--/options-->
|
||||
|
||||
## ℹ️ Examples workflows
|
||||
|
||||
<!--examples-->
|
||||
<!--/examples-->
|
||||
7
source/plugins/sponsorships/examples.yml
Normal file
7
source/plugins/sponsorships/examples.yml
Normal file
@@ -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
|
||||
51
source/plugins/sponsorships/index.mjs
Normal file
51
source/plugins/sponsorships/index.mjs
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
41
source/plugins/sponsorships/metadata.yml
Normal file
41
source/plugins/sponsorships/metadata.yml
Normal file
@@ -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
|
||||
29
source/plugins/sponsorships/queries/all.graphql
Normal file
29
source/plugins/sponsorships/queries/all.graphql
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
source/plugins/sponsorships/queries/sponsorships.graphql
Normal file
10
source/plugins/sponsorships/queries/sponsorships.graphql
Normal file
@@ -0,0 +1,10 @@
|
||||
query SponsorshipsDefault {
|
||||
$account(login: "$login") {
|
||||
totalSponsorshipAmountAsSponsorInCents
|
||||
sponsorshipsAsSponsor(first: 1, activeOnly: false, orderBy: {field: CREATED_AT, direction: ASC}) {
|
||||
nodes {
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
source/templates/classic/partials/_.json
vendored
1
source/templates/classic/partials/_.json
vendored
@@ -39,6 +39,7 @@
|
||||
"code",
|
||||
"chess",
|
||||
"sponsors",
|
||||
"sponsorships",
|
||||
"poopmap",
|
||||
"fortune",
|
||||
"splatoon"
|
||||
|
||||
40
source/templates/classic/partials/sponsorships.ejs
vendored
Normal file
40
source/templates/classic/partials/sponsorships.ejs
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
<% if (plugins.sponsorships) { %>
|
||||
<section>
|
||||
<h2 class="field">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path d="M10.586 1C12.268 1 13.5 2.37 13.5 4.25c0 1.745-.996 3.359-2.622 4.831-.166.15-.336.297-.509.438l1.116 5.584a.75.75 0 0 1-.991.852l-2.409-.876a.25.25 0 0 0-.17 0l-2.409.876a.75.75 0 0 1-.991-.852L5.63 9.519a13.78 13.78 0 0 1-.51-.438C3.497 7.609 2.5 5.995 2.5 4.25 2.5 2.37 3.732 1 5.414 1c.963 0 1.843.403 2.474 1.073L8 2.198l.112-.125a3.385 3.385 0 0 1 2.283-1.068L10.586 1Zm-3.621 9.495-.718 3.594 1.155-.42a1.75 1.75 0 0 1 1.028-.051l.168.051 1.154.42-.718-3.592c-.199.13-.37.235-.505.314l-.169.097a.75.75 0 0 1-.72 0 9.54 9.54 0 0 1-.515-.308l-.16-.105ZM10.586 2.5c-.863 0-1.611.58-1.866 1.459-.209.721-1.231.721-1.44 0C7.025 3.08 6.277 2.5 5.414 2.5 4.598 2.5 4 3.165 4 4.25c0 1.23.786 2.504 2.128 3.719.49.443 1.018.846 1.546 1.198l.325.21.076-.047.251-.163a13.341 13.341 0 0 0 1.546-1.198C11.214 6.754 12 5.479 12 4.25c0-1.085-.598-1.75-1.414-1.75Z"></path></svg>
|
||||
Sponsorships
|
||||
</h2>
|
||||
<% if (plugins.sponsorships.error) { %>
|
||||
<div class="row fill-width">
|
||||
<section>
|
||||
<div class="field error">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16"><path fill-rule="evenodd" d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"></path></svg>
|
||||
<%= plugins.sponsorships.error.message %>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<% } else { %>
|
||||
<% for (const section of plugins.sponsorships.sections) { %>
|
||||
<div class="row fill-width">
|
||||
<% if (section === "amount") { %>
|
||||
<section class="sponsorships sponsors goal">
|
||||
<img src="<%= plugins.sponsorships.image %>" alt="" />
|
||||
<div><span class="bold"><%= user.login %></span> has given a total of <span class="bold"><%= new Intl.NumberFormat("en", {style:"currency", currency:"USD"}).format(plugins.sponsorships.amount) %></span> to open source software<% if (plugins.sponsorships.started) { %> since <span class="bold"><%= f.date(plugins.sponsorships.started, { date:true, timeZone:config.timezone?.name}) %></span><% } %>.</div>
|
||||
</section>
|
||||
<% } else if (section === "sponsorships") { %>
|
||||
<section class="sponsors goal">
|
||||
<div class="goal-text">
|
||||
<span>
|
||||
<%= user.login %> helped funding the work of <%= plugins.sponsorships.list.length %> users and organizations.
|
||||
</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<% for (const user of plugins.sponsorships.list) { %><img class="avatar <%= user.past ? 'past' : '' %> <%= user.type === "organization" ? "organization" : "" %>" src="<%= user.avatar %>" width="<%= 0.8*plugins.sponsorships.size %>" height="<%= 0.8*plugins.sponsorships.size %>" alt="" /><% } %>
|
||||
</div>
|
||||
</section>
|
||||
<% } %>
|
||||
</div>
|
||||
<% } %>
|
||||
<% } %>
|
||||
</section>
|
||||
<% } %>
|
||||
11
source/templates/classic/style.css
vendored
11
source/templates/classic/style.css
vendored
@@ -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 {
|
||||
|
||||
33
tests/mocks/api/github/graphql/sponsorships.all.mjs
Normal file
33
tests/mocks/api/github/graphql/sponsorships.all.mjs
Normal file
@@ -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,
|
||||
},
|
||||
})),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
12
tests/mocks/api/github/graphql/sponsorships.default.mjs
Normal file
12
tests/mocks/api/github/graphql/sponsorships.default.mjs
Normal file
@@ -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()}`}]
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user