Skip to content

Commit

Permalink
Support [Matrix] summary endpoint (badges#10782)
Browse files Browse the repository at this point in the history
* feat: add support for Matrix summary endpoint

* docs: update Matrix documentation

* refactor: abstract logic into more functions

* refactor: revert custom timeout for tests

* test: add test for fetchMode override on matrix.org

* fix: validate the fetchMode value

* docs: extend fetchMode description

* fix: add await keywords

* clarify test description

---------

Co-authored-by: chris48s <[email protected]>
  • Loading branch information
cyb3rko and chris48s authored Jan 3, 2025
1 parent 18e1723 commit 2c65301
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 24 deletions.
94 changes: 72 additions & 22 deletions services/matrix/matrix.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,15 @@ import {
pathParam,
queryParam,
} from '../index.js'
import { nonNegativeInteger } from '../validators.js'

const fetchModeEnum = ['guest', 'summary']

const queryParamSchema = Joi.object({
server_fqdn: Joi.string().hostname(),
fetchMode: Joi.string()
.valid(...fetchModeEnum)
.default('guest'),
}).required()

const matrixRegisterSchema = Joi.object({
Expand All @@ -31,9 +37,16 @@ const matrixStateSchema = Joi.array()
)
.required()

const matrixSummarySchema = Joi.object({
num_joined_members: nonNegativeInteger,
}).required()

const description = `
In order for this badge to work, the host of your room must allow guest accounts or dummy accounts to register, and the room must be world readable (chat history visible to anyone).
Alternatively access via the experimental <code>summary</code> endpoint ([MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)) can be configured with the query parameter <code>fetchMode</code> for less server load and better performance, if supported by the homeserver<br/>
For the <code>matrix.org</code> homeserver <code>fetchMode</code> is hard-coded to <code>summary</code>.
The following steps will show you how to setup the badge URL using the Element Matrix client.
<ul>
Expand Down Expand Up @@ -76,6 +89,15 @@ export default class Matrix extends BaseJsonService {
name: 'server_fqdn',
example: 'matrix.org',
}),
queryParam({
name: 'fetchMode',
example: 'guest',
description: `<code>guest</code> configures guest authentication while <code>summary</code> configures usage of the experimental "summary" endpoint ([MSC3266](https://github.com/matrix-org/matrix-spec-proposals/pull/3266)). If not specified, the default fetch mode is <code>guest</code> (except for matrix.org).`,
schema: {
type: 'string',
enum: fetchModeEnum,
},
}),
],
},
},
Expand Down Expand Up @@ -147,27 +169,27 @@ export default class Matrix extends BaseJsonService {
})
}

async fetch({ roomAlias, serverFQDN }) {
let host
if (serverFQDN === undefined) {
const splitAlias = roomAlias.split(':')
// A room alias can either be in the form #localpart:server or
// #localpart:server:port.
switch (splitAlias.length) {
case 2:
host = splitAlias[1]
break
case 3:
host = `${splitAlias[1]}:${splitAlias[2]}`
break
default:
throw new InvalidParameter({ prettyMessage: 'invalid alias' })
}
} else {
host = serverFQDN
}
async fetchSummary({ host, roomAlias }) {
const data = await this._requestJson({
url: `https://${host}/_matrix/client/unstable/im.nheko.summary/rooms/%23${encodeURIComponent(
roomAlias,
)}/summary`,
schema: matrixSummarySchema,
httpErrors: {
400: 'unknown request',
404: 'room or endpoint not found',
},
})
return data.num_joined_members
}

async fetchGuest({ host, roomAlias }) {
const accessToken = await this.retrieveAccessToken({ host })
const lookup = await this.lookupRoomAlias({ host, roomAlias, accessToken })
const lookup = await this.lookupRoomAlias({
host,
roomAlias,
accessToken,
})
const data = await this._requestJson({
url: `https://${host}/_matrix/client/r0/rooms/${encodeURIComponent(
lookup.room_id,
Expand All @@ -194,8 +216,36 @@ export default class Matrix extends BaseJsonService {
: 0
}

async handle({ roomAlias }, { server_fqdn: serverFQDN }) {
const members = await this.fetch({ roomAlias, serverFQDN })
async fetch({ roomAlias, serverFQDN, fetchMode }) {
let host
if (serverFQDN === undefined) {
const splitAlias = roomAlias.split(':')
// A room alias can either be in the form #localpart:server or
// #localpart:server:port.
switch (splitAlias.length) {
case 2:
host = splitAlias[1]
break
case 3:
host = `${splitAlias[1]}:${splitAlias[2]}`
break
default:
throw new InvalidParameter({ prettyMessage: 'invalid alias' })
}
} else {
host = serverFQDN
}
if (host.toLowerCase() === 'matrix.org' || fetchMode === 'summary') {
// summary endpoint (default for matrix.org)
return await this.fetchSummary({ host, roomAlias })
} else {
// guest access
return await this.fetchGuest({ host, roomAlias })
}
}

async handle({ roomAlias }, { server_fqdn: serverFQDN, fetchMode }) {
const members = await this.fetch({ roomAlias, serverFQDN, fetchMode })
return this.constructor.render({ members })
}
}
133 changes: 131 additions & 2 deletions services/matrix/matrix.tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,26 @@ t.create('get room state as member (backup method)')
color: 'brightgreen',
})

t.create('get room summary')
.get('/ALIAS:DUMMY.dumb.json?fetchMode=summary')
.intercept(nock =>
nock('https://DUMMY.dumb/')
.get(
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb/summary',
)
.reply(
200,
JSON.stringify({
num_joined_members: 4,
}),
),
)
.expectBadge({
label: 'chat',
message: '4 users',
color: 'brightgreen',
})

t.create('bad server or connection')
.get('/ALIAS:DUMMY.dumb.json')
.networkOff()
Expand Down Expand Up @@ -263,6 +283,27 @@ t.create('unknown request')
color: 'lightgrey',
})

t.create('unknown summary request')
.get('/ALIAS:DUMMY.dumb.json?fetchMode=summary')
.intercept(nock =>
nock('https://DUMMY.dumb/')
.get(
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb/summary',
)
.reply(
400,
JSON.stringify({
errcode: 'M_UNRECOGNIZED',
error: 'Unrecognized request',
}),
),
)
.expectBadge({
label: 'chat',
message: 'unknown request',
color: 'lightgrey',
})

t.create('unknown alias')
.get('/ALIAS:DUMMY.dumb.json')
.intercept(nock =>
Expand Down Expand Up @@ -291,6 +332,27 @@ t.create('unknown alias')
color: 'red',
})

t.create('unknown summary alias')
.get('/ALIAS:DUMMY.dumb.json?fetchMode=summary')
.intercept(nock =>
nock('https://DUMMY.dumb/')
.get(
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb/summary',
)
.reply(
404,
JSON.stringify({
errcode: 'M_NOT_FOUND',
error: 'Room alias #ALIAS%3ADUMMY.dumb not found.',
}),
),
)
.expectBadge({
label: 'chat',
message: 'room or endpoint not found',
color: 'red',
})

t.create('invalid alias').get('/ALIASDUMMY.dumb.json').expectBadge({
label: 'chat',
message: 'invalid alias',
Expand Down Expand Up @@ -368,6 +430,26 @@ t.create('server uses a custom port')
color: 'brightgreen',
})

t.create('server uses a custom port for summary')
.get('/ALIAS:DUMMY.dumb:5555.json?fetchMode=summary')
.intercept(nock =>
nock('https://DUMMY.dumb:5555/')
.get(
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb%3A5555/summary',
)
.reply(
200,
JSON.stringify({
num_joined_members: 4,
}),
),
)
.expectBadge({
label: 'chat',
message: '4 users',
color: 'brightgreen',
})

t.create('specify the homeserver fqdn')
.get('/ALIAS:DUMMY.dumb.json?server_fqdn=matrix.DUMMY.dumb')
.intercept(nock =>
Expand Down Expand Up @@ -439,9 +521,56 @@ t.create('specify the homeserver fqdn')
color: 'brightgreen',
})

t.create('test on real matrix room for API compliance')
t.create('specify the homeserver fqdn for summary')
.get('/ALIAS:DUMMY.dumb.json?server_fqdn=matrix.DUMMY.dumb&fetchMode=summary')
.intercept(nock =>
nock('https://matrix.DUMMY.dumb/')
.get(
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb/summary',
)
.reply(
200,
JSON.stringify({
num_joined_members: 4,
}),
),
)
.expectBadge({
label: 'chat',
message: '4 users',
color: 'brightgreen',
})

t.create('test fetchMode=guest is ignored for matrix.org')
.get('/ALIAS:DUMMY.dumb.json?server_fqdn=matrix.org&fetchMode=guest')
.intercept(nock =>
nock('https://matrix.org/')
.get(
'/_matrix/client/unstable/im.nheko.summary/rooms/%23ALIAS%3ADUMMY.dumb/summary',
)
.reply(
200,
JSON.stringify({
num_joined_members: 4,
}),
),
)
.expectBadge({
label: 'chat',
message: '4 users',
color: 'brightgreen',
})

t.create('test on real matrix room for guest API compliance')
.get('/ndcube:openastronomy.org.json?server_fqdn=openastronomy.modular.im')
.expectBadge({
label: 'chat',
message: Joi.string().regex(/^[0-9]+ users$/),
color: 'brightgreen',
})

t.create('test on real matrix room for summary API compliance')
.get('/twim:matrix.org.json')
.timeout(10000)
.expectBadge({
label: 'chat',
message: Joi.string().regex(/^[0-9]+ users$/),
Expand Down

0 comments on commit 2c65301

Please sign in to comment.