diff --git a/services/matrix/matrix.service.js b/services/matrix/matrix.service.js index a6199a35ec697..4c9b78114ede1 100644 --- a/services/matrix/matrix.service.js +++ b/services/matrix/matrix.service.js @@ -2,11 +2,20 @@ const Joi = require('joi') const BaseJsonService = require('../base-json') +const errors = require('../errors') + +const queryParamSchema = Joi.object({ + server_fqdn: Joi.string().hostname(), +}).required() const matrixRegisterSchema = Joi.object({ access_token: Joi.string().required(), }).required() +const matrixAliasLookupSchema = Joi.object({ + room_id: Joi.string().required(), +}) + const matrixStateSchema = Joi.array() .items( Joi.object({ @@ -31,15 +40,36 @@ const documentation = `
Advanced
tabInternal room ID
with your rooms ID next to it (ex: !ltIjvaLydYAWZyihee:matrix.org
):
with /
/matrix/!ltIjvaLydYAWZyihee/matrix.org.svg
Addresses
sectionroom addresses (or aliases)
, which can be easily identified with their starting hash (#
) character (ex: #twim:matrix.org
)Local addresses for this room
#
)/matrix/twim:matrix.org.svg
example.com
that created the room alias #mysuperroom:example.com
lives at matrix.example.com
).
+
+ If that is the case of the homeserver that created the room alias used for generating the badge, you will need to add the server's FQDN (fully qualified domain name) as a query parameter.
+
+ The final badge URL should then look something like this /matrix/mysuperroom:example.com.svg?server_fqdn=matrix.example.com
.
`
module.exports = class Matrix extends BaseJsonService {
+ async retrieveAccessToken({ host }) {
+ let auth
+ try {
+ auth = await this.registerAccount({ host, guest: true })
+ } catch (e) {
+ if (e.prettyMessage === 'guests not allowed') {
+ // attempt fallback method
+ auth = await this.registerAccount({ host, guest: false })
+ } else throw e
+ }
+
+ return auth.access_token
+ }
+
async registerAccount({ host, guest }) {
return this._requestJson({
url: `https://${host}/_matrix/client/r0/register`,
@@ -59,27 +89,59 @@ module.exports = class Matrix extends BaseJsonService {
errorMessages: {
401: 'auth failed',
403: 'guests not allowed',
- 429: 'rate limited by rooms host',
+ 429: 'rate limited by remote server',
},
})
}
- async fetch({ host, roomId }) {
- let auth
- try {
- auth = await this.registerAccount({ host, guest: true })
- } catch (e) {
- if (e.prettyMessage === 'guests not allowed') {
- // attempt fallback method
- auth = await this.registerAccount({ host, guest: false })
- } else throw e
+ async lookupRoomAlias({ host, roomAlias, accessToken }) {
+ return this._requestJson({
+ url: `https://${host}/_matrix/client/r0/directory/room/${encodeURIComponent(
+ `#${roomAlias}`
+ )}`,
+ schema: matrixAliasLookupSchema,
+ options: {
+ qs: {
+ access_token: accessToken,
+ },
+ },
+ errorMessages: {
+ 401: 'bad auth token',
+ 404: 'room not found',
+ 429: 'rate limited by remote server',
+ },
+ })
+ }
+
+ 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 errors.InvalidParameter({ prettyMessage: 'invalid alias' })
+ }
+ } 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/${roomId}/state`,
+ url: `https://${host}/_matrix/client/r0/rooms/${encodeURIComponent(
+ lookup.room_id
+ )}/state`,
schema: matrixStateSchema,
options: {
qs: {
- access_token: auth.access_token,
+ access_token: accessToken,
},
},
errorMessages: {
@@ -109,11 +171,12 @@ module.exports = class Matrix extends BaseJsonService {
}
}
- async handle({ roomId, host, authServer }) {
- const members = await this.fetch({
- host,
- roomId: `${roomId}:${host}`,
- })
+ async handle({ roomAlias }, queryParams) {
+ const { server_fqdn: serverFQDN } = this.constructor._validateQueryParams(
+ queryParams,
+ queryParamSchema
+ )
+ const members = await this.fetch({ roomAlias, serverFQDN })
return this.constructor.render({ members })
}
@@ -128,8 +191,9 @@ module.exports = class Matrix extends BaseJsonService {
static get route() {
return {
base: 'matrix',
- format: '([^/]+)/([^/]+)',
- capture: ['roomId', 'host'],
+ format: '([^/]+)',
+ capture: ['roomAlias'],
+ queryParams: ['server_fqdn'],
}
}
@@ -137,8 +201,16 @@ module.exports = class Matrix extends BaseJsonService {
return [
{
title: 'Matrix',
- exampleUrl: '!ltIjvaLydYAWZyihee/matrix.org',
- pattern: ':roomId/:host',
+ namedParams: { roomAlias: 'twim:matrix.org' },
+ pattern: ':roomAlias',
+ staticExample: this.render({ members: 42 }),
+ documentation,
+ },
+ {
+ title: 'Matrix',
+ namedParams: { roomAlias: 'twim:matrix.org' },
+ queryParams: { server_fqdn: 'matrix.org' },
+ pattern: ':roomAlias',
staticExample: this.render({ members: 42 }),
documentation,
},
diff --git a/services/matrix/matrix.tester.js b/services/matrix/matrix.tester.js
index 225b452577840..1bbf5915a597a 100644
--- a/services/matrix/matrix.tester.js
+++ b/services/matrix/matrix.tester.js
@@ -1,14 +1,12 @@
'use strict'
const Joi = require('joi')
-const ServiceTester = require('../service-tester')
const { colorScheme } = require('../test-helpers')
-const t = new ServiceTester({ id: 'matrix', title: 'Matrix' })
-module.exports = t
+const t = (module.exports = require('../create-service-tester')())
t.create('get room state as guest')
- .get('/ROOM/DUMMY.dumb.json?style=_shields_test')
+ .get('/ALIAS:DUMMY.dumb.json?style=_shields_test')
.intercept(nock =>
nock('https://DUMMY.dumb/')
.post('/_matrix/client/r0/register?kind=guest')
@@ -18,7 +16,18 @@ t.create('get room state as guest')
access_token: 'TOKEN',
})
)
- .get('/_matrix/client/r0/rooms/ROOM:DUMMY.dumb/state?access_token=TOKEN')
+ .get(
+ '/_matrix/client/r0/directory/room/%23ALIAS%3ADUMMY.dumb?access_token=TOKEN'
+ )
+ .reply(
+ 200,
+ JSON.stringify({
+ room_id: 'ROOM:DUMMY.dumb',
+ })
+ )
+ .get(
+ '/_matrix/client/r0/rooms/ROOM%3ADUMMY.dumb/state?access_token=TOKEN'
+ )
.reply(
200,
JSON.stringify([
@@ -68,14 +77,14 @@ t.create('get room state as guest')
})
t.create('get room state as member (backup method)')
- .get('/ROOM/DUMMY.dumb.json?style=_shields_test')
+ .get('/ALIAS:DUMMY.dumb.json?style=_shields_test')
.intercept(nock =>
nock('https://DUMMY.dumb/')
.post('/_matrix/client/r0/register?kind=guest')
.reply(
403,
JSON.stringify({
- errcode: 'M_GUEST_ACCESS_FORBIDDEN', // i think this is the right one
+ errcode: 'M_GUEST_ACCESS_FORBIDDEN',
error: 'Guest access not allowed',
})
)
@@ -86,7 +95,18 @@ t.create('get room state as member (backup method)')
access_token: 'TOKEN',
})
)
- .get('/_matrix/client/r0/rooms/ROOM:DUMMY.dumb/state?access_token=TOKEN')
+ .get(
+ '/_matrix/client/r0/directory/room/%23ALIAS%3ADUMMY.dumb?access_token=TOKEN'
+ )
+ .reply(
+ 200,
+ JSON.stringify({
+ room_id: 'ROOM:DUMMY.dumb',
+ })
+ )
+ .get(
+ '/_matrix/client/r0/rooms/ROOM%3ADUMMY.dumb/state?access_token=TOKEN'
+ )
.reply(
200,
JSON.stringify([
@@ -136,7 +156,7 @@ t.create('get room state as member (backup method)')
})
t.create('bad server or connection')
- .get('/ROOM/DUMMY.dumb.json?style=_shields_test')
+ .get('/ALIAS:DUMMY.dumb.json?style=_shields_test')
.networkOff()
.expectJSON({
name: 'chat',
@@ -144,8 +164,8 @@ t.create('bad server or connection')
colorB: colorScheme.lightgray,
})
-t.create('invalid room')
- .get('/ROOM/DUMMY.dumb.json?style=_shields_test')
+t.create('non-world readable room')
+ .get('/ALIAS:DUMMY.dumb.json?style=_shields_test')
.intercept(nock =>
nock('https://DUMMY.dumb/')
.post('/_matrix/client/r0/register?kind=guest')
@@ -155,7 +175,18 @@ t.create('invalid room')
access_token: 'TOKEN',
})
)
- .get('/_matrix/client/r0/rooms/ROOM:DUMMY.dumb/state?access_token=TOKEN')
+ .get(
+ '/_matrix/client/r0/directory/room/%23ALIAS%3ADUMMY.dumb?access_token=TOKEN'
+ )
+ .reply(
+ 200,
+ JSON.stringify({
+ room_id: 'ROOM:DUMMY.dumb',
+ })
+ )
+ .get(
+ '/_matrix/client/r0/rooms/ROOM%3ADUMMY.dumb/state?access_token=TOKEN'
+ )
.reply(
403,
JSON.stringify({
@@ -171,7 +202,7 @@ t.create('invalid room')
})
t.create('invalid token')
- .get('/ROOM/DUMMY.dumb.json?style=_shields_test')
+ .get('/ALIAS:DUMMY.dumb.json?style=_shields_test')
.intercept(nock =>
nock('https://DUMMY.dumb/')
.post('/_matrix/client/r0/register?kind=guest')
@@ -181,7 +212,9 @@ t.create('invalid token')
access_token: 'TOKEN',
})
)
- .get('/_matrix/client/r0/rooms/ROOM:DUMMY.dumb/state?access_token=TOKEN')
+ .get(
+ '/_matrix/client/r0/directory/room/%23ALIAS%3ADUMMY.dumb?access_token=TOKEN'
+ )
.reply(
401,
JSON.stringify({
@@ -197,7 +230,7 @@ t.create('invalid token')
})
t.create('unknown request')
- .get('/ROOM/DUMMY.dumb.json?style=_shields_test')
+ .get('/ALIAS:DUMMY.dumb.json?style=_shields_test')
.intercept(nock =>
nock('https://DUMMY.dumb/')
.post('/_matrix/client/r0/register?kind=guest')
@@ -207,7 +240,18 @@ t.create('unknown request')
access_token: 'TOKEN',
})
)
- .get('/_matrix/client/r0/rooms/ROOM:DUMMY.dumb/state?access_token=TOKEN')
+ .get(
+ '/_matrix/client/r0/directory/room/%23ALIAS%3ADUMMY.dumb?access_token=TOKEN'
+ )
+ .reply(
+ 200,
+ JSON.stringify({
+ room_id: 'ROOM:DUMMY.dumb',
+ })
+ )
+ .get(
+ '/_matrix/client/r0/rooms/ROOM%3ADUMMY.dumb/state?access_token=TOKEN'
+ )
.reply(
400,
JSON.stringify({
@@ -222,8 +266,189 @@ t.create('unknown request')
colorB: colorScheme.lightgray,
})
+t.create('unknown alias')
+ .get('/ALIAS:DUMMY.dumb.json?style=_shields_test')
+ .intercept(nock =>
+ nock('https://DUMMY.dumb/')
+ .post('/_matrix/client/r0/register?kind=guest')
+ .reply(
+ 200,
+ JSON.stringify({
+ access_token: 'TOKEN',
+ })
+ )
+ .get(
+ '/_matrix/client/r0/directory/room/%23ALIAS%3ADUMMY.dumb?access_token=TOKEN'
+ )
+ .reply(
+ 404,
+ JSON.stringify({
+ errcode: 'M_NOT_FOUND',
+ error: 'Room alias #ALIAS%3ADUMMY.dumb not found.',
+ })
+ )
+ )
+ .expectJSON({
+ name: 'chat',
+ value: 'room not found',
+ colorB: colorScheme.red,
+ })
+
+t.create('invalid alias')
+ .get('/ALIASDUMMY.dumb.json?style=_shields_test')
+ .expectJSON({
+ name: 'chat',
+ value: 'invalid alias',
+ colorB: colorScheme.red,
+ })
+
+t.create('server uses a custom port')
+ .get('/ALIAS:DUMMY.dumb:5555.json?style=_shields_test')
+ .intercept(nock =>
+ nock('https://DUMMY.dumb:5555/')
+ .post('/_matrix/client/r0/register?kind=guest')
+ .reply(
+ 200,
+ JSON.stringify({
+ access_token: 'TOKEN',
+ })
+ )
+ .get(
+ '/_matrix/client/r0/directory/room/%23ALIAS%3ADUMMY.dumb%3A5555?access_token=TOKEN'
+ )
+ .reply(
+ 200,
+ JSON.stringify({
+ room_id: 'ROOM:DUMMY.dumb:5555',
+ })
+ )
+ .get(
+ '/_matrix/client/r0/rooms/ROOM%3ADUMMY.dumb%3A5555/state?access_token=TOKEN'
+ )
+ .reply(
+ 200,
+ JSON.stringify([
+ {
+ // valid user 1
+ type: 'm.room.member',
+ sender: '@user1:DUMMY.dumb:5555',
+ state_key: '@user1:DUMMY.dumb:5555',
+ content: {
+ membership: 'join',
+ },
+ },
+ {
+ // valid user 2
+ type: 'm.room.member',
+ sender: '@user2:DUMMY.dumb:5555',
+ state_key: '@user2:DUMMY.dumb:5555',
+ content: {
+ membership: 'join',
+ },
+ },
+ {
+ // should exclude banned/invited/left members
+ type: 'm.room.member',
+ sender: '@user3:DUMMY.dumb:5555',
+ state_key: '@user3:DUMMY.dumb:5555',
+ content: {
+ membership: 'leave',
+ },
+ },
+ {
+ // exclude events like the room name
+ type: 'm.room.name',
+ sender: '@user4:DUMMY.dumb:5555',
+ state_key: '@user4:DUMMY.dumb:5555',
+ content: {
+ membership: 'fake room',
+ },
+ },
+ ])
+ )
+ )
+ .expectJSON({
+ name: 'chat',
+ value: '2 users',
+ colorB: colorScheme.brightgreen,
+ })
+
+t.create('specify the homeserver fqdn')
+ .get(
+ '/ALIAS:DUMMY.dumb.json?style=_shields_test&server_fqdn=matrix.DUMMY.dumb'
+ )
+ .intercept(nock =>
+ nock('https://matrix.DUMMY.dumb/')
+ .post('/_matrix/client/r0/register?kind=guest')
+ .reply(
+ 200,
+ JSON.stringify({
+ access_token: 'TOKEN',
+ })
+ )
+ .get(
+ '/_matrix/client/r0/directory/room/%23ALIAS%3ADUMMY.dumb?access_token=TOKEN'
+ )
+ .reply(
+ 200,
+ JSON.stringify({
+ room_id: 'ROOM:DUMMY.dumb',
+ })
+ )
+ .get(
+ '/_matrix/client/r0/rooms/ROOM%3ADUMMY.dumb/state?access_token=TOKEN'
+ )
+ .reply(
+ 200,
+ JSON.stringify([
+ {
+ // valid user 1
+ type: 'm.room.member',
+ sender: '@user1:DUMMY.dumb',
+ state_key: '@user1:DUMMY.dumb',
+ content: {
+ membership: 'join',
+ },
+ },
+ {
+ // valid user 2
+ type: 'm.room.member',
+ sender: '@user2:DUMMY.dumb',
+ state_key: '@user2:DUMMY.dumb',
+ content: {
+ membership: 'join',
+ },
+ },
+ {
+ // should exclude banned/invited/left members
+ type: 'm.room.member',
+ sender: '@user3:DUMMY.dumb',
+ state_key: '@user3:DUMMY.dumb',
+ content: {
+ membership: 'leave',
+ },
+ },
+ {
+ // exclude events like the room name
+ type: 'm.room.name',
+ sender: '@user4:DUMMY.dumb',
+ state_key: '@user4:DUMMY.dumb',
+ content: {
+ membership: 'fake room',
+ },
+ },
+ ])
+ )
+ )
+ .expectJSON({
+ name: 'chat',
+ value: '2 users',
+ colorB: colorScheme.brightgreen,
+ })
+
t.create('test on real matrix room for API compliance')
- .get('/!ltIjvaLydYAWZyihee/matrix.org.json?style=_shields_test')
+ .get('/twim:matrix.org.json?style=_shields_test')
+ .timeout(10000)
.expectJSONTypes(
Joi.object().keys({
name: 'chat',