From df1e40cb1e3f20af7cc341e49fbe1d377f787451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dario=20Vladovi=C4=87?= Date: Tue, 16 Feb 2021 05:53:16 +0100 Subject: [PATCH] feat(matrix): introduce Gitter and Matrix member count badges (#500) * feat: add Gitter members badge * feat: add Matrix members badge with Gitter fallback * docs(matrix): update examples * perf(matrix): fail fast if room does NOT exist * style(gitter): tweak scrapping pattern --- api/gitter.ts | 39 +++++++++++++++++++++ api/matrix.ts | 84 ++++++++++++++++++++++++++++++++++++++++++++++ libs/badge-list.ts | 3 ++ 3 files changed, 126 insertions(+) create mode 100644 api/gitter.ts create mode 100644 api/matrix.ts diff --git a/api/gitter.ts b/api/gitter.ts new file mode 100644 index 0000000..71b23df --- /dev/null +++ b/api/gitter.ts @@ -0,0 +1,39 @@ +import got from '../libs/got' +import { millify } from '../libs/utils' +import { createBadgenHandler, PathArgs } from '../libs/create-badgen-handler' + +const BRAND_COLOR = 'ED1965' + +export default createBadgenHandler({ + title: 'Gitter', + examples: { + '/gitter/members/redom/lobby': 'members', + '/gitter/members/redom/redom': 'members' + }, + handlers: { + '/gitter/members/:org/:room': handler + } +}) + +async function handler ({ org, room }: PathArgs) { + const membersCount = await fetchMembersCount(org, room) + if (Number.isNaN(membersCount)) { + return { + subject: 'gitter', + status: 'unknown', + color: 'grey' + } + } + + const suffix = membersCount === 1 ? 'member' : 'members' + return { + subject: 'gitter', + status: `${millify(membersCount)} ${suffix}`, + color: BRAND_COLOR + } +} + +export async function fetchMembersCount(org: string, room: string) { + const html = await got(`https://gitter.im/${org}/${room}`).text() + return Number(html.match(/"userCount"\s*:\s*(\d+)/)?.[1]) +} diff --git a/api/matrix.ts b/api/matrix.ts new file mode 100644 index 0000000..8cf5651 --- /dev/null +++ b/api/matrix.ts @@ -0,0 +1,84 @@ +import { Got } from 'got' +import got from '../libs/got' +import { millify } from '../libs/utils' +import { fetchMembersCount as fetchGitterMembersCount } from './gitter' +import { createBadgenHandler, PathArgs } from '../libs/create-badgen-handler' + +const BRAND_COLOR = 'black' + +export default createBadgenHandler({ + title: 'Matrix', + examples: { + '/matrix/members/rust/matrix.org': 'members', + '/matrix/members/thisweekinmatrix': 'members', + '/matrix/members/archlinux/archlinux.org': 'members', + '/matrix/members/redom_redom/gitter.im': 'members' + }, + handlers: { + '/matrix/members/:room/:server?': handler + } +}) + +async function handler ({ room, server = 'matrix.org' }: PathArgs) { + const roomName = room.replace(/^#/, '') + const membersCount = await fetchMembersCount(roomName, server) + if (Number.isNaN(membersCount)) { + return { + subject: 'matrix', + status: 'unknown', + color: 'grey' + } + } + + const status = [ + millify(membersCount), + server === 'gitter.im' ? 'gitter' : '', + membersCount === 1 ? 'member' : 'members' + ].join(' ') + + return { + subject: `#${roomName}:${server}`, + status, + color: BRAND_COLOR + } +} + +async function fetchMembersCount(roomName: string, server: string) { + if (server === 'gitter.im') { + const [gitterOrg, gitterRoom] = roomName.split('_') + return fetchGitterMembersCount(gitterOrg, gitterRoom) + } + const homeserver = await getHomeserver(server) + const client = got.extend({ prefixUrl: `${homeserver}/_matrix/client/r0` }) + const roomAlias = `#${roomName}:${server}` + const room = await findPublicRoom(client, roomAlias) + return room?.num_joined_members +} + +// https://matrix.org/docs/spec/client_server/latest#get-well-known-matrix-client +async function getHomeserver(server: string) { + const endpoint = `https://${server}/.well-known/matrix/client` + const { 'm.homeserver': homeserver } = await got(endpoint).json() + return homeserver?.base_url +} + +// https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-publicrooms +async function findPublicRoom(client: Got, roomAlias: string) { + const roomId = await getRoomId(client, roomAlias) + const searchParams = new URLSearchParams({ limit: '500' }) + // eslint-disable-next-line no-constant-condition + while (true) { + const { chunk, next_batch } = await client.get('publicRooms', { searchParams }).json() + const room = chunk.find(it => it.room_id === roomId) + if (room) return room + if (!next_batch) return + searchParams.set('since', next_batch) + } +} + +// https://matrix.org/docs/spec/client_server/latest#get-matrix-client-r0-directory-room-roomalias +async function getRoomId(client: Got, roomAlias: string) { + const endpoint = `directory/room/${encodeURIComponent(roomAlias)}` + const { room_id } = await client.get(endpoint).json() + return room_id +} diff --git a/libs/badge-list.ts b/libs/badge-list.ts index d1f75d2..be107e7 100644 --- a/libs/badge-list.ts +++ b/libs/badge-list.ts @@ -61,6 +61,9 @@ export const liveBadgeList = [ // social 'devrant', 'peertube', + // chat + 'gitter', + 'matrix', // utilities 'opencollective', 'keybase',