Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support [Matrix] summary endpoint #10782

Merged
merged 9 commits into from
Jan 3, 2025
Merged
94 changes: 66 additions & 28 deletions services/matrix/matrix.service.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import {
pathParam,
queryParam,
} from '../index.js'
import { nonNegativeInteger } from '../validators.js'

const queryParamSchema = Joi.object({
server_fqdn: Joi.string().hostname(),
fetchMode: Joi.string().optional(),
}).required()

const matrixRegisterSchema = Joi.object({
Expand All @@ -31,9 +33,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 +85,15 @@ export default class Matrix extends BaseJsonService {
name: 'server_fqdn',
example: 'matrix.org',
}),
queryParam({
name: 'fetchMode',
example: 'guest',
description: `If not specified, the default fetch mode is <code>guest</code> (except for matrix.org).`,
chris48s marked this conversation as resolved.
Show resolved Hide resolved
schema: {
type: 'string',
enum: ['guest', 'summary'],
chris48s marked this conversation as resolved.
Show resolved Hide resolved
},
}),
],
},
},
Expand Down Expand Up @@ -147,7 +165,7 @@ export default class Matrix extends BaseJsonService {
})
}

async fetch({ roomAlias, serverFQDN }) {
async fetch({ roomAlias, serverFQDN, fetchMode }) {
let host
if (serverFQDN === undefined) {
const splitAlias = roomAlias.split(':')
Expand All @@ -166,36 +184,56 @@ export default class Matrix extends BaseJsonService {
} else {
host = serverFQDN
}
const accessToken = await this.retrieveAccessToken({ host })
const lookup = await this.lookupRoomAlias({ host, roomAlias, accessToken })
const data = await this._requestJson({
url: `https://${host}/_matrix/client/r0/rooms/${encodeURIComponent(
lookup.room_id,
)}/state`,
schema: matrixStateSchema,
options: {
searchParams: {
access_token: accessToken,
if (host.toLowerCase() === 'matrix.org' || fetchMode === 'summary') {
chris48s marked this conversation as resolved.
Show resolved Hide resolved
// summary endpoint (default for matrix.org)
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',
},
},
httpErrors: {
400: 'unknown request',
401: 'bad auth token',
403: 'room not world readable or is invalid',
},
})
return Array.isArray(data)
? data.filter(
m =>
m.type === 'm.room.member' &&
m.sender === m.state_key &&
m.content.membership === 'join',
).length
: 0
})
return data.num_joined_members
} else {
// guest access
const accessToken = await this.retrieveAccessToken({ host })
const lookup = await this.lookupRoomAlias({
host,
roomAlias,
accessToken,
})
const data = await this._requestJson({
url: `https://${host}/_matrix/client/r0/rooms/${encodeURIComponent(
lookup.room_id,
)}/state`,
schema: matrixStateSchema,
options: {
searchParams: {
access_token: accessToken,
},
},
httpErrors: {
400: 'unknown request',
401: 'bad auth token',
403: 'room not world readable or is invalid',
},
})
return Array.isArray(data)
? data.filter(
m =>
m.type === 'm.room.member' &&
m.sender === m.state_key &&
m.content.membership === 'join',
).length
: 0
}
}

async handle({ roomAlias }, { server_fqdn: serverFQDN }) {
const members = await this.fetch({ roomAlias, serverFQDN })
async handle({ roomAlias }, { server_fqdn: serverFQDN, fetchMode }) {
const members = await this.fetch({ roomAlias, serverFQDN, fetchMode })
return this.constructor.render({ members })
}
}
113 changes: 112 additions & 1 deletion services/matrix/matrix.tester.js
chris48s marked this conversation as resolved.
Show resolved Hide resolved
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,7 +521,36 @@ 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 on real matrix room for guest API compliance')
.get('/ndcube:openastronomy.org.json?server_fqdn=openastronomy.modular.im')
.timeout(10000)
.expectBadge({
label: 'chat',
message: Joi.string().regex(/^[0-9]+ users$/),
color: 'brightgreen',
})

t.create('test on real matrix room for summary API compliance')
chris48s marked this conversation as resolved.
Show resolved Hide resolved
.get('/twim:matrix.org.json')
.timeout(10000)
chris48s marked this conversation as resolved.
Show resolved Hide resolved
.expectBadge({
Expand Down
Loading