Skip to content

Commit

Permalink
Add [Matrix] Badge (badges#2417)
Browse files Browse the repository at this point in the history
* Added matrix badge

* decreased the size of the matrix logo by more than 50%

* returning the size in fetch() instead of an object

* found another way to register a throwaway account (guest account). this one actually works on matrix.org, but I kept the old way as a backup method. also changed the POST from /members to /state because guest accounts didn't work with /members

* updated logo to a recolored version of the official logo

* Removed unnecessary comments.
Added documentation on how to create the badge URL.
Added a test that hits a real room to test for API compliance.
URLs are now obtained from getter functions.
Added JSON schema for the /state API request.
Improved state response filter.
Replaced example URL room ID to a dedicated testing room.
Made some error messages more helpful.

* correctly implemented requested changes

* changed color hex codes to constants
  • Loading branch information
fr1kin authored and chris48s committed Dec 6, 2018
1 parent 499ea36 commit 9e95020
Show file tree
Hide file tree
Showing 4 changed files with 382 additions and 0 deletions.
1 change: 1 addition & 0 deletions lib/project/critical-services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ paulmelnikow:
- hexpm
- jenkins
- luarocks
- matrix
- maven-central
- node
- nom
Expand Down
1 change: 1 addition & 0 deletions logo/matrix.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
147 changes: 147 additions & 0 deletions services/matrix/matrix.service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
'use strict'

const Joi = require('joi')
const BaseJsonService = require('../base-json')

const matrixRegisterSchema = Joi.object({
access_token: Joi.string().required(),
}).required()

const matrixStateSchema = Joi.array()
.items(
Joi.object({
content: Joi.object({
membership: Joi.string().optional(),
}).required(),
type: Joi.string().required(),
sender: Joi.string().required(),
state_key: Joi.string()
.allow('')
.required(),
})
)
.required()

const documentation = `
<p>
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).
</br>
The following steps will show you how to setup the badge URL using the Riot.im Matrix client.
</br>
<ul>
<li>Select the desired room inside the Riot.im client</li>
<li>Click on the room settings button (gear icon) located near the top right of the client</li>
<li>Scroll to the very bottom of the settings page and look under the <code>Advanced</code> tab</li>
<li>You should see the <code>Internal room ID</code> with your rooms ID next to it (ex: <code>!ltIjvaLydYAWZyihee:matrix.org</code>)</li>
<li>Replace the IDs <code>:</code> with <code>/</code></li>
<li>The final badge URL should look something like this <code>/matrix/!ltIjvaLydYAWZyihee/matrix.org.svg</code></li>
</ul>
</p>
`

module.exports = class Matrix extends BaseJsonService {
async registerAccount({ host, guest }) {
return this._requestJson({
url: `https://${host}/_matrix/client/r0/register`,
schema: matrixRegisterSchema,
options: {
method: 'POST',
qs: guest
? {
kind: 'guest',
}
: {},
body: JSON.stringify({
password: '',
auth: { type: 'm.login.dummy' },
}),
},
errorMessages: {
401: 'auth failed',
403: 'guests not allowed',
429: 'rate limited by rooms host',
},
})
}

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
}
const data = await this._requestJson({
url: `https://${host}/_matrix/client/r0/rooms/${roomId}/state`,
schema: matrixStateSchema,
options: {
qs: {
access_token: auth.access_token,
},
},
errorMessages: {
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
}

static get _cacheLength() {
return 30
}

static render({ members }) {
return {
message: `${members} users`,
color: 'brightgreen',
}
}

async handle({ roomId, host, authServer }) {
const members = await this.fetch({
host,
roomId: `${roomId}:${host}`,
})
return this.constructor.render({ members })
}

static get defaultBadgeData() {
return { label: 'chat' }
}

static get category() {
return 'chat'
}

static get route() {
return {
base: 'matrix',
format: '([^/]+)/([^/]+)',
capture: ['roomId', 'host'],
}
}

static get examples() {
return [
{
title: 'Matrix',
exampleUrl: '!ltIjvaLydYAWZyihee/matrix.org',
pattern: ':roomId/:host',
staticExample: this.render({ members: 42 }),
documentation,
},
]
}
}
233 changes: 233 additions & 0 deletions services/matrix/matrix.tester.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
'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

t.create('get room state as guest')
.get('/ROOM/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/rooms/ROOM:DUMMY.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('get room state as member (backup method)')
.get('/ROOM/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
error: 'Guest access not allowed',
})
)
.post('/_matrix/client/r0/register')
.reply(
200,
JSON.stringify({
access_token: 'TOKEN',
})
)
.get('/_matrix/client/r0/rooms/ROOM:DUMMY.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('bad server or connection')
.get('/ROOM/DUMMY.dumb.json?style=_shields_test')
.networkOff()
.expectJSON({
name: 'chat',
value: 'inaccessible',
colorB: colorScheme.lightgray,
})

t.create('invalid room')
.get('/ROOM/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/rooms/ROOM:DUMMY.dumb/state?access_token=TOKEN')
.reply(
403,
JSON.stringify({
errcode: 'M_GUEST_ACCESS_FORBIDDEN',
error: 'Guest access not allowed',
})
)
)
.expectJSON({
name: 'chat',
value: 'room not world readable or is invalid',
colorB: colorScheme.lightgray,
})

t.create('invalid token')
.get('/ROOM/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/rooms/ROOM:DUMMY.dumb/state?access_token=TOKEN')
.reply(
401,
JSON.stringify({
errcode: 'M_UNKNOWN_TOKEN',
error: 'Unrecognised access token.',
})
)
)
.expectJSON({
name: 'chat',
value: 'bad auth token',
colorB: colorScheme.lightgray,
})

t.create('unknown request')
.get('/ROOM/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/rooms/ROOM:DUMMY.dumb/state?access_token=TOKEN')
.reply(
400,
JSON.stringify({
errcode: 'M_UNRECOGNIZED',
error: 'Unrecognized request',
})
)
)
.expectJSON({
name: 'chat',
value: 'unknown request',
colorB: colorScheme.lightgray,
})

t.create('test on real matrix room for API compliance')
.get('/!ltIjvaLydYAWZyihee/matrix.org.json?style=_shields_test')
.expectJSONTypes(
Joi.object().keys({
name: 'chat',
value: Joi.string().regex(/^[0-9]+ users$/),
colorB: colorScheme.brightgreen,
})
)

0 comments on commit 9e95020

Please sign in to comment.