Skip to content

Commit 4879ed0

Browse files
authored
People plugin : repository template support (lowlighter#78)
1 parent c075d49 commit 4879ed0

File tree

14 files changed

+343
-63
lines changed

14 files changed

+343
-63
lines changed

README.md

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,21 @@ But there's more with [plugins](https://github.com/lowlighter/metrics/tree/maste
181181
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.people.following.svg" alt="" width="400">
182182
</a>
183183
</details>
184+
<details><summary>Special thanks version</summary>
185+
<a href="#-habits">
186+
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.people.thanks.svg" alt="" width="400">
187+
</a>
188+
</details>
189+
<details><summary>Repository template version</summary>
190+
<a href="#-habits">
191+
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.people.repository.svg" alt="" width="400">
192+
</a>
193+
</details>
194+
<details><summary>Sponsorships version</summary>
195+
<a href="#-habits">
196+
<img src="https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.people.sponsorships.svg" alt="" width="400">
197+
</a>
198+
</details>
184199
</td>
185200
</tr>
186201
<tr>
@@ -1528,7 +1543,8 @@ Add the following to your workflow:
15281543

15291544
### 🧑‍🤝‍🧑 People
15301545

1531-
The *people* plugin displays your followers and followed users' avatars.
1546+
The *people* plugin can display people you're following or sponsoring, and also users who're following or sponsoring you.
1547+
In repository mode, it's possible to display sponsors, stargazers, watchers.
15321548

15331549
![People plugin](https://github.com/lowlighter/lowlighter/blob/master/metrics.plugin.people.svg)
15341550

@@ -1537,6 +1553,24 @@ The *people* plugin displays your followers and followed users' avatars.
15371553

15381554
It will consume an additional GitHub request per group of 100 users fetched.
15391555

1556+
The following types are supported:
1557+
1558+
| Type | Alias | User metrics | Repository metrics |
1559+
| --------------- | ------------------------------------ | :----------------: | :----------------: |
1560+
| `followers` | | ✔️ | ❌ |
1561+
| `following` | `followed` | ✔️ | ❌ |
1562+
| `sponsoring`* | `sponsored`, `sponsorshipsAsSponsor` | ✔️ | ❌ |
1563+
| `sponsors`* | `sponsorshipsAsMaintainer` | ✔️ | ✔️ |
1564+
| `contributors`* | | ❌ | ✔️ |
1565+
| `stargazers`* | | ❌ | ✔️ |
1566+
| `watchers`* | | ❌ | ✔️ |
1567+
| `thanks`* | | ✔️ | ✔️ |
1568+
1569+
🚧 Types marked with * are available as pre-release on @master branch (unstable)
1570+
1571+
Sections will be ordered the same as specified in `plugin_people_types`.
1572+
`sponsors` for repositories will output the same as the owner's sponsors.
1573+
15401574
Add the following to your workflow:
15411575
```yaml
15421576
- uses: lowlighter/metrics@latest
@@ -1557,6 +1591,17 @@ It is possible to use [identicons](https://github.blog/2013-08-14-identicons/) i
15571591
plugin_people_identicons: yes
15581592
```
15591593

1594+
🚧 This feature is available as pre-release on @master branch (unstable)
1595+
1596+
It is possible to thanks personally users by adding the following to your workflow:
1597+
```yaml
1598+
- uses: lowlighter/metrics@master
1599+
with:
1600+
# ... other options
1601+
plugin_people_types: thanks
1602+
plugin_people_thanks: github-user-1, github-user-2, ...
1603+
```
1604+
15601605
</details>
15611606

15621607
### 🌸 Anilist

action.yml

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -469,13 +469,29 @@ inputs:
469469
default: 28
470470

471471
# List of users categories to display (comma separated)
472-
# Supported values are:
472+
# For user's metrics, supported values are:
473473
# - "followers"
474-
# - "following"
474+
# - "following"/"followed"
475+
# - "sponsors"/"sponsorshipsAsMaintainer"
476+
# - "sponsoring"/"sponsored"/"sponsorshipsAsSponsor"
477+
# - "thanks" (see "plugin_people_thanks" below)
478+
# For repositories' metrics, supported values are:
479+
# - "contributors"
480+
# - "stargazers"
481+
# - "watchers"
482+
# - "sponsors"/"sponsorshipsAsMaintainer"
483+
# - "thanks" (see "plugin_people_thanks" below)
475484
plugin_people_types:
476485
description: Categories to display
477486
default: followers, following
478487

488+
# List of users to thanks (comma seperated)
489+
# When using "thanks" as a type, it'll display the users you listed in this option
490+
# This can be used to create "Special thanks" badges that you can embed elsewhere
491+
plugin_people_thanks:
492+
description: Users to thanks in "thanks" section type
493+
default: ""
494+
479495
# Display GitHub identicons instead of users' real avatar
480496
# Mostly for privacy purposes
481497
plugin_people_identicons:

source/app/action/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@
239239
if (plugins.people.enabled) {
240240
for (const option of ["limit", "size"])
241241
info(`People ${option}`, q[`people.${option}`] = input.number(`plugin_people_${option}`))
242-
for (const option of ["types"])
242+
for (const option of ["types", "thanks"])
243243
info(`People ${option}`, q[`people.${option}`] = input.array(`plugin_people_${option}`))
244244
for (const option of ["identicons"])
245245
info(`People ${option}`, q[`people.${option}`] = input.bool(`plugin_people_${option}`))

source/app/mocks.mjs

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@
362362
}) : ({
363363
user:{
364364
[type]:{
365-
edges:new Array(Math.ceil(20+80*Math.random())).fill(undefined).map((login = faker.internet.userName()) => ({
365+
edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map((login = faker.internet.userName()) => ({
366366
cursor:"MOCKED_CURSOR",
367367
node:{
368368
login,
@@ -373,6 +373,66 @@
373373
}
374374
})
375375
}
376+
//People query (repositories)
377+
if (/^query PeopleRepository /.test(query)) {
378+
console.debug(`metrics/compute/mocks > mocking graphql api result > People`)
379+
const type = query.match(/(?<type>stargazers|watchers)[(]/)?.groups?.type ?? "(unknown type)"
380+
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
381+
user:{
382+
repository:{
383+
[type]:{
384+
edges:[],
385+
}
386+
}
387+
}
388+
}) : ({
389+
user:{
390+
repository:{
391+
[type]:{
392+
edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map((login = faker.internet.userName()) => ({
393+
cursor:"MOCKED_CURSOR",
394+
node:{
395+
login,
396+
avatarUrl:null,
397+
}
398+
}))
399+
}
400+
}
401+
}
402+
})
403+
}
404+
//People sponsors query
405+
if (/^query PeopleSponsors /.test(query)) {
406+
console.debug(`metrics/compute/mocks > mocking graphql api result > People`)
407+
const type = query.match(/(?<type>sponsorshipsAsSponsor|sponsorshipsAsMaintainer)[(]/)?.groups?.type ?? "(unknown type)"
408+
return /after: "MOCKED_CURSOR"/m.test(query) ? ({
409+
user:{
410+
login,
411+
[type]:{
412+
edges:[]
413+
}
414+
}
415+
}) : ({
416+
user:{
417+
login,
418+
[type]:{
419+
edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map((login = faker.internet.userName()) => ({
420+
cursor:"MOCKED_CURSOR",
421+
node:{
422+
sponsorEntity:{
423+
login:faker.internet.userName(),
424+
avatarUrl:null,
425+
},
426+
sponsorable:{
427+
login:faker.internet.userName(),
428+
avatarUrl:null,
429+
}
430+
}
431+
}))
432+
}
433+
}
434+
})
435+
}
376436
//Unmocked call
377437
return target(...args)
378438
}
@@ -389,6 +449,8 @@
389449
getViews:rest.repos.getViews,
390450
getContributorsStats:rest.repos.getContributorsStats,
391451
listCommits:rest.repos.listCommits,
452+
listContributors:rest.repos.listContributors,
453+
getByUsername:rest.users.getByUsername,
392454
}
393455

394456
//Raw request
@@ -893,6 +955,49 @@
893955
})
894956
}
895957
})
958+
959+
//Repository contributors
960+
rest.repos.listContributors = new Proxy(unmocked.listContributors, {
961+
apply:function(target, that, [{owner, repo}]) {
962+
console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.listContributors`)
963+
return ({
964+
status:200,
965+
url:`https://api.github.com/repos/${owner}/${repo}/contributors`,
966+
headers: {
967+
server:"GitHub.com",
968+
status:"200 OK",
969+
"x-oauth-scopes":"repo",
970+
},
971+
data:new Array(40+faker.random.number(60)).fill(null).map(() => ({
972+
login:faker.internet.userName(),
973+
avatar_url:null,
974+
contributions:faker.random.number(1000),
975+
}))
976+
})
977+
}
978+
})
979+
980+
//User informations
981+
rest.users.getByUsername = new Proxy(unmocked.getByUsername, {
982+
apply:function(target, that, [{username}]) {
983+
console.debug(`metrics/compute/mocks > mocking rest api result > rest.repos.getByUsername`)
984+
return ({
985+
status:200,
986+
url:`'https://api.github.com/users/${username}/`,
987+
headers: {
988+
server:"GitHub.com",
989+
status:"200 OK",
990+
"x-oauth-scopes":"repo",
991+
},
992+
data:{
993+
login:faker.internet.userName(),
994+
avatar_url:null,
995+
contributions:faker.random.number(1000),
996+
}
997+
})
998+
}
999+
})
1000+
8961001
}
8971002

8981003
//Axios mocking

source/app/web/statics/app.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
stars:"🌟 Recently starred repositories",
7777
stargazers:"✨ Stargazers over last weeks",
7878
activity:"📰 Recent activity",
79-
people:"🧑‍🤝‍🧑 Followers and followed",
79+
people:"🧑‍🤝‍🧑 People",
8080
anilist:"🌸 Anilist",
8181
base:"🗃️ Base content",
8282
"base.header":"Header",
@@ -120,6 +120,7 @@
120120
"people.size":{text:"Limit", type:"number", min:16, max:64},
121121
"people.limit":{text:"Limit", type:"number", min:1, max:9999},
122122
"people.types":{text:"Types", placeholder:"followers, following"},
123+
"people.thanks":{text:"Special thanks", placeholder:"user1, user2, ..."},
123124
"people.identicons":{text:"Use identicons", type:"boolean"},
124125
"anilist.medias":{text:"Medias to display", placeholder:"anime, manga"},
125126
"anilist.sections":{text:"Sections to display", placeholder:"favorites, watching, reading, characters"},
@@ -157,6 +158,7 @@
157158
"people.size":28,
158159
"people.limit":28,
159160
"people.types":"followers, following",
161+
"people.thanks":"",
160162
"people.identicons":false,
161163
"anilist.medias":"anime, manga",
162164
"anilist.sections":"favorites",

source/app/web/statics/app.placeholder.js

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -222,17 +222,25 @@
222222
}) : null),
223223
//People
224224
...(set.plugins.enabled.people ? ({
225-
people:{
226-
types:options["people.types"].split(",").map(x => x.trim()),
227-
size:options["people.size"],
228-
followers:new Array(Number(options["people.limit"])).fill(null).map(_ => ({
229-
login:faker.internet.userName(),
230-
avatar:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
231-
})),
232-
following:new Array(Number(options["people.limit"])).fill(null).map(_ => ({
233-
login:faker.internet.userName(),
234-
avatar:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
235-
}))
225+
get people() {
226+
const types = options["people.types"].split(",").map(x => x.trim())
227+
.map(x => ({followed:"following", sponsors:"sponsorshipsAsMaintainer", sponsored:"sponsorshipsAsSponsor", sponsoring:"sponsorshipsAsSponsor"})[x] ?? x)
228+
.filter(x => ["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor"].includes(x))
229+
return {
230+
types,
231+
size:options["people.size"],
232+
...(Object.fromEntries(types.map(type => [
233+
type,
234+
new Array(Number(options["people.limit"])).fill(null).map(_ => ({
235+
login:faker.internet.userName(),
236+
avatar:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
237+
}))
238+
]))),
239+
thanks:options["people.thanks"].split(",").map(x => x.trim()).map(login => ({
240+
login,
241+
avatar:"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mOcOnfpfwAGfgLYttYINwAAAABJRU5ErkJggg==",
242+
}))
243+
}
236244
}
237245
}) : null),
238246
//Music

source/plugins/people/index.mjs

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,66 @@
11
//Setup
2-
export default async function ({login, graphql, q, queries, imports}, {enabled = false} = {}) {
2+
export default async function ({login, data, graphql, rest, q, queries, imports}, {enabled = false} = {}) {
33
//Plugin execution
44
try {
55
//Check if plugin is enabled and requirements are met
66
if ((!enabled)||(!q.people))
77
return null
88

9+
//Context
10+
let context = {
11+
mode:"user",
12+
types:["followers", "following", "sponsorshipsAsMaintainer", "sponsorshipsAsSponsor", "thanks"],
13+
default:"followers, following",
14+
alias:{followed:"following", sponsors:"sponsorshipsAsMaintainer", sponsored:"sponsorshipsAsSponsor", sponsoring:"sponsorshipsAsSponsor"},
15+
sponsorships:{sponsorshipsAsMaintainer:"sponsorEntity", sponsorshipsAsSponsor:"sponsorable"}
16+
}
17+
if (q.repo) {
18+
console.debug(`metrics/compute/${login}/plugins > people > switched to repository mode`)
19+
const {owner, repo} = data.user.repositories.nodes.map(({name:repo, owner:{login:owner}}) => ({repo, owner})).shift()
20+
context = {...context, mode:"repo", types:["contributors", "stargazers", "watchers", "sponsorshipsAsMaintainer", "thanks"], default:"stargazers, watchers", owner, repo}
21+
}
22+
923
//Parameters override
10-
let {"people.limit":limit = 28, "people.types":types = "followers, following", "people.size":size = 28, "people.identicons":identicons = false} = q
24+
let {"people.limit":limit = 28, "people.types":types = context.default, "people.size":size = 28, "people.identicons":identicons = false, "people.thanks":thanks = []} = q
1125
//Limit
1226
limit = Math.max(1, limit)
1327
//Repositories projects
14-
types = decodeURIComponent(types ?? "").split(",").map(type => type.trim()).filter(type => ["followers", "following"].includes(type)) ?? []
28+
types = [...new Set(decodeURIComponent(types ?? "").split(",").map(type => type.trim()).map(type => (context.alias[type] ?? type)).filter(type => context.types.includes(type)) ?? [])]
29+
//Special thanks
30+
thanks = decodeURIComponent(thanks ?? "").split(",").map(user => user.trim()).filter(user => user)
1531

1632
//Retrieve followers from graphql api
1733
console.debug(`metrics/compute/${login}/plugins > people > querying api`)
18-
const result = {followers:[], following:[]}
34+
const result = Object.fromEntries(types.map(type => [type, []]))
1935
for (const type of types) {
2036
//Iterate through people
2137
console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type}`)
22-
let cursor = null
23-
let pushed = 0
24-
do {
25-
console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type} after ${cursor}`)
26-
const {user:{[type]:{edges}}} = await graphql(queries.people({login, type, size, after:cursor ? `after: "${cursor}"` : ""}))
27-
cursor = edges?.[edges?.length-1]?.cursor
28-
result[type].push(...edges.map(({node}) => node))
29-
pushed = edges.length
30-
} while ((pushed)&&(cursor))
38+
//Rest
39+
if (type === "contributors") {
40+
const {owner, repo} = context
41+
const {data:nodes} = await rest.repos.listContributors({owner, repo})
42+
result[type].push(...nodes.map(({login, avatar_url}) => ({login, avatarUrl:avatar_url})))
43+
}
44+
else if (type === "thanks") {
45+
const nodes = await Promise.all(thanks.map(async username => (await rest.users.getByUsername({username})).data))
46+
result[type].push(...nodes.map(({login, avatar_url}) => ({login, avatarUrl:avatar_url})))
47+
}
48+
//GraphQL
49+
else {
50+
let cursor = null
51+
let pushed = 0
52+
do {
53+
console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type} after ${cursor}`)
54+
const {[type]:{edges}} = (
55+
type in context.sponsorships ? (await graphql(queries["people.sponsors"]({login:context.owner ?? login, type, size, after:cursor ? `after: "${cursor}"` : "", target:context.sponsorships[type]}))).user :
56+
context.mode === "repo" ? (await graphql(queries["people.repository"]({login:context.owner, repository:context.repo, type, size, after:cursor ? `after: "${cursor}"` : ""}))).user.repository :
57+
(await graphql(queries.people({login, type, size, after:cursor ? `after: "${cursor}"` : ""}))).user
58+
)
59+
cursor = edges?.[edges?.length-1]?.cursor
60+
result[type].push(...edges.map(({node}) => node[context.sponsorships[type]] ?? node))
61+
pushed = edges.length
62+
} while ((pushed)&&(cursor)&&(result[type].length <= limit))
63+
}
3164
//Limit people
3265
if (limit > 0) {
3366
console.debug(`metrics/compute/${login}/plugins > people > keeping only ${limit} ${type}`)

0 commit comments

Comments
 (0)