Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
Copy link
Copy Markdown

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
Copy Markdown
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
Copy Markdown
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') {
// 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