diff --git a/README.md b/README.md
index 440a91ba..3f826b57 100644
--- a/README.md
+++ b/README.md
@@ -164,7 +164,7 @@ But there's more with [plugins](https://github.com/lowlighter/metrics/tree/maste
🎫 Gists plugin
- 🗃️ Header special features
+ 🧑🤝🧑 People plugin 🚧 @master
@@ -172,11 +172,28 @@ But there's more with [plugins](https://github.com/lowlighter/metrics/tree/maste
+
+
+
+
+ Followed people version
+
+
+
+
+
+
+
+ 🗃️ Header special features
+
+
+
+
@@ -534,6 +551,7 @@ Used template defaults to the `classic` one.
🌟
✨
🎫
+ 🧑🤝🧑
Classic
@@ -550,10 +568,11 @@ Used template defaults to the `classic` one.
✔️
✔️
✔️
- ✔️M
+ ✔️M
✔️
✔️
✔️
+ ✔️M
Terminal
@@ -574,6 +593,7 @@ Used template defaults to the `classic` one.
❌
❌
✔️
+ ❌
RepositoryR
@@ -594,6 +614,7 @@ Used template defaults to the `classic` one.
❌
✔️
❌
+ ❌
@@ -1381,6 +1402,41 @@ Add the following to your workflow :
+### 🧑🤝🧑 People
+
+ 🚧 This plugin is available as pre-release on @master branch (unstable)
+
+The *people* plugin displays your followers and followed users' avatars.
+
+
+
+
+💬 About
+
+It will consume an additional GitHub request per group of 100 users fetched.
+
+Add the following to your workflow :
+```yaml
+- uses: lowlighter/metrics@master
+ with:
+ # ... other options
+ plugin_people: yes
+ plugin_people_types: followers, following
+ plugin_people_limit: 28
+ plugin_people_size: 28 # Size in pixels of displayed avatars
+```
+
+It is possible to use [identicons](https://github.blog/2013-08-14-identicons/) instead of their avatar for privacy purposes.
+
+```yaml
+- uses: lowlighter/metrics@master
+ with:
+ # ... other options
+ plugin_people_identicons: yes
+```
+
+
+
### 🔧 Other options
A few additional options are available.
diff --git a/action.yml b/action.yml
index f952af5e..ad4a5480 100644
--- a/action.yml
+++ b/action.yml
@@ -377,7 +377,7 @@ inputs:
description: Number of activity events to display
default: 5
- # Disacard older events
+ # Discard older events
# Use 0 to display activity whatever the date
plugin_activity_days:
description: Maximum activity event age
@@ -400,6 +400,35 @@ inputs:
description: Events to display
default: all
+ # Display followed and following users
+ plugin_people:
+ description: Display
+ default: no
+
+ # Limit the number of users displayed
+ plugin_people_limit:
+ description: Number of users to display per categorie
+ default: 28
+
+ # Configure image size of users' avatar
+ plugin_people_size:
+ description: Size of users' avatars
+ default: 28
+
+ # List of users categories to display (comma separated)
+ # Supported values are:
+ # - "followers"
+ # - "following"
+ plugin_people_types:
+ description: Categories to display
+ default: followers, following
+
+ # Display GitHub identicons instead of users' real avatar
+ # Mostly for privacy purposes
+ plugin_people_identicons:
+ description: Use identicons instead of real avatars
+ default: no
+
# ====================================================================================
# Options below are mostly used for testing
@@ -497,7 +526,7 @@ runs:
set -e
echo "Is released version: $METRICS_IS_RELEASED"
# Rebuild image for unreleased version
- if [[ $METRICS_IS_RELEASED ]]; then
+ if [[ "$METRICS_IS_RELEASED" -gt "0" ]]; then
echo "Using released version $METRICS_TAG, will pull docker image from GitHub registry"
METRICS_IMAGE=ghcr.io/lowlighter/metrics:$METRICS_TAG
# Use registry for released version
diff --git a/settings.example.json b/settings.example.json
index 6049aed6..3db0a2fb 100644
--- a/settings.example.json
+++ b/settings.example.json
@@ -67,6 +67,9 @@
},
"activity":{ "//":"Activity plugin",
"enabled":false, "//":"Enable or disable recent activity display"
+ },
+ "people":{ "//":"People plugin",
+ "enabled":false, "//":"Enable or disable people display"
}
}
}
\ No newline at end of file
diff --git a/source/app/action/index.mjs b/source/app/action/index.mjs
index e9f8d8ef..fceb7f3f 100644
--- a/source/app/action/index.mjs
+++ b/source/app/action/index.mjs
@@ -142,6 +142,7 @@
stars:{enabled:input.bool("plugin_stars")},
stargazers:{enabled:input.bool("plugin_stargazers")},
activity:{enabled:input.bool("plugin_activity")},
+ people:{enabled:input.bool("plugin_people")},
}
let q = Object.fromEntries(Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => [key, true]))
info("Plugins enabled", Object.entries(plugins).filter(([key, plugin]) => plugin.enabled).map(([key]) => key))
@@ -223,6 +224,15 @@
for (const option of ["filter"])
info(`Activity ${option}`, q[`activity.${option}`] = input.array(`plugin_activity_${option}`))
}
+ //People
+ if (plugins.people.enabled) {
+ for (const option of ["limit", "size"])
+ info(`People ${option}`, q[`people.${option}`] = input.number(`plugin_people_${option}`))
+ for (const option of ["types"])
+ info(`People ${option}`, q[`people.${option}`] = input.array(`plugin_people_${option}`))
+ for (const option of ["identicons"])
+ info(`People ${option}`, q[`people.${option}`] = input.bool(`plugin_people_${option}`))
+ }
//Repositories to use
const repositories = input.number("repositories")
diff --git a/source/app/metrics.mjs b/source/app/metrics.mjs
index 50f859ae..1a6b2cae 100644
--- a/source/app/metrics.mjs
+++ b/source/app/metrics.mjs
@@ -90,7 +90,7 @@
//Additional SVG transformations
if (/svg/.test(mime)) {
//Optimize rendering
- if ((conf.optimize)&&(!q.raw)) {
+ if ((conf.settings?.optimize)&&(!q.raw)) {
console.debug(`metrics/compute/${login} > optimize`)
const svgo = new SVGO({full:true, plugins:[{cleanupAttrs:true}, {inlineStyles:false}]})
const {data:optimized} = await svgo.optimize(rendered)
diff --git a/source/app/mocks.mjs b/source/app/mocks.mjs
index f0e19e13..9846e5f6 100644
--- a/source/app/mocks.mjs
+++ b/source/app/mocks.mjs
@@ -316,6 +316,30 @@
}
})
}
+ //People query
+ if (/^query People /.test(query)) {
+ console.debug(`metrics/compute/mocks > mocking graphql api result > People`)
+ const type = query.match(/(?followers|following)[(]/)?.groups?.type ?? "(unknown type)"
+ return /after: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"/m.test(query) ? ({
+ user:{
+ [type]:{
+ edges:[],
+ }
+ }
+ }) : ({
+ user:{
+ [type]:{
+ edges:new Array(Math.ceil(20+80*Math.random())).fill(null).map(() => ({
+ cursor:"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ node:{
+ login:"user",
+ avatarUrl:"https://github.com/identicons/user.png",
+ }
+ }))
+ }
+ }
+ })
+ }
//Unmocked call
return target(...args)
}
diff --git a/source/plugins/people/index.mjs b/source/plugins/people/index.mjs
new file mode 100644
index 00000000..18a38d9e
--- /dev/null
+++ b/source/plugins/people/index.mjs
@@ -0,0 +1,53 @@
+//Setup
+ export default async function ({login, graphql, q, queries, imports}, {enabled = false} = {}) {
+ //Plugin execution
+ try {
+ //Check if plugin is enabled and requirements are met
+ if ((!enabled)||(!q.people))
+ return null
+
+ //Parameters override
+ let {"people.limit":limit = 28, "people.types":types = "followers, following", "people.size":size = 28, "people.identicons":identicons = false} = q
+ //Limit
+ limit = Math.max(1, limit)
+ //Repositories projects
+ types = decodeURIComponent(types ?? "").split(",").map(type => type.trim()).filter(type => ["followers", "following"].includes(type)) ?? []
+
+ //Retrieve followers from graphql api
+ console.debug(`metrics/compute/${login}/plugins > people > querying api`)
+ const result = {followers:[], following:[]}
+ for (const type of types) {
+ //Iterate through people
+ console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type}`)
+ let cursor = null
+ let pushed = 0
+ do {
+ console.debug(`metrics/compute/${login}/plugins > people > retrieving ${type} after ${cursor}`)
+ const {user:{[type]:{edges}}} = await graphql(queries.people({login, type, size, after:cursor ? `after: "${cursor}"` : ""}))
+ cursor = edges?.[edges?.length-1]?.cursor
+ result[type].push(...edges.map(({node}) => node))
+ pushed = edges.length
+ } while ((pushed)&&(cursor))
+ //Limit people
+ if (limit > 0) {
+ console.debug(`metrics/compute/${login}/plugins > people > keeping only ${limit} ${type}`)
+ result[type].splice(limit)
+ }
+ //Hide real avator with identicons if enabled
+ if (identicons) {
+ console.debug(`metrics/compute/${login}/plugins > people > using identicons`)
+ result[type].map(user => user.avatarUrl = `https://github.com/identicons/${user.login}.png`)
+ }
+ //Convert avatars to base64
+ console.debug(`metrics/compute/${login}/plugins > people > loading avatars`)
+ await Promise.all(result[type].map(async user => user.avatar = await imports.imgb64(user.avatarUrl)))
+ }
+
+ //Results
+ return {types, size, ...result}
+ }
+ //Handle errors
+ catch (error) {
+ throw {error:{message:"An error occured", instance:error}}
+ }
+ }
diff --git a/source/plugins/projects/index.mjs b/source/plugins/projects/index.mjs
index b70d1aa2..bb6d70d2 100644
--- a/source/plugins/projects/index.mjs
+++ b/source/plugins/projects/index.mjs
@@ -8,7 +8,7 @@
//Parameters override
let {"projects.limit":limit = 4, "projects.repositories":repositories = ""} = q
//Repositories projects
- repositories = repositories?.split(",").map(repository => repository.trim()).filter(repository => /[-\w]+[/][-\w]+[/]projects[/]\d+/.test(repository)) ?? []
+ repositories = decodeURIComponent(repositories ?? "").split(",").map(repository => repository.trim()).filter(repository => /[-\w]+[/][-\w]+[/]projects[/]\d+/.test(repository)) ?? []
//Limit
limit = Math.max(repositories.length, Math.min(100, Number(limit)))
//Retrieve user owned projects from graphql api
diff --git a/source/queries/people.graphql b/source/queries/people.graphql
new file mode 100644
index 00000000..c69041eb
--- /dev/null
+++ b/source/queries/people.graphql
@@ -0,0 +1,14 @@
+query People {
+ user(login: "$login") {
+ login
+ $type($after first: 100) {
+ edges {
+ cursor
+ node {
+ login
+ avatarUrl(size: $size)
+ }
+ }
+ }
+ }
+}
diff --git a/source/templates/classic/image.svg b/source/templates/classic/image.svg
index c5f1afe9..5719973f 100644
--- a/source/templates/classic/image.svg
+++ b/source/templates/classic/image.svg
@@ -895,6 +895,66 @@
<% } %>
+ <% if (plugins.people) { %>
+ <% if (plugins.people.error) { %>
+
+
+
+
+
+
+
+
+
+ <%= plugins.people.error.message %>
+
+
+
+
+ <% } else { %>
+ <% if (plugins.people.types?.includes("followers")) { %>
+
+
+
+ <%= user.followers.totalCount %> follower<%= s(user.followers.totalCount) %>
+
+
+
+ <% if (plugins.people.error) { %>
+
+
+ <%= plugins.people.error.message %>
+
+ <% } else { %>
+ <% for (const user of plugins.people.followers) { %> <% } %>
+ <% } %>
+
+
+
+ <% } %>
+ <% if (plugins.people.types?.includes("following")) { %>
+
+
+
+ <%= user.following.totalCount %> followed
+
+
+
+ <% if (plugins.people.error) { %>
+
+
+ <%= plugins.people.error.message %>
+
+ <% } else { %>
+ <% for (const user of plugins.people.following) { %> <% } %>
+ <% } %>
+
+
+
+ <% } %>
+ <% } %>
+ <% } %>
+
<% if (plugins.activity) { %>
diff --git a/source/templates/classic/style.css b/source/templates/classic/style.css
index e2ecb764..9ae70f5f 100644
--- a/source/templates/classic/style.css
+++ b/source/templates/classic/style.css
@@ -85,7 +85,6 @@
/* User avatar */
.avatar {
- background-color: #000000;
border-radius: 50%;
margin: 0 6px;
}
@@ -503,6 +502,15 @@
-webkit-box-orient: vertical;
}
+/* People */
+ .people {
+ padding: 0 10px;
+ }
+
+ .people .avatar {
+ margin: 0 2px;
+ }
+
/* Fade animation */
.af {
opacity: 0;
diff --git a/tests/metrics.test.js b/tests/metrics.test.js
index b4cfc80c..80f7c6fd 100644
--- a/tests/metrics.test.js
+++ b/tests/metrics.test.js
@@ -255,6 +255,21 @@
["Gists plugin (default)", {
plugin_gists:true,
}, {skip:["terminal", "repository"]}],
+ ["People plugin (default)", {
+ plugin_people:true,
+ }, {skip:["terminal", "repository"]}],
+ ["People plugin (followers)", {
+ plugin_people:true,
+ plugin_people_types:"followers",
+ }, {skip:["terminal", "repository"]}],
+ ["People plugin (following)", {
+ plugin_people:true,
+ plugin_people_types:"following",
+ }, {skip:["terminal", "repository"]}],
+ ["People plugin (identicons)", {
+ plugin_people:true,
+ plugin_people_identicons:true,
+ }, {skip:["terminal", "repository"]}],
]
//Tests run