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: 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()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for fixing this!
any chance we can still make this snake_case without breaking the world, though? 🙈

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the base attributes for shields badges use camel case.
'server_fqdn' should be camel case as well but that would be a breaking change, so that's how we ended up here ;)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bah. I should have picked this up in review.
Tbh, this is more of a codebase-wide mess than that.
I've raised an issue at #10804 about how to approach this as a wider issue.

.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') {
chris48s marked this conversation as resolved.
Show resolved Hide resolved
// 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
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,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
Loading