Skip to content

Commit 8fd581f

Browse files
authored
✨implement 3pid binding invite hook ( #158 ) (#187)
* ✨ feat: add list invitation tokens db method * 🧑‍💻 feat: added server name extractor helper * ✨ feat: call onbind hook * 🏷️ chore: added related types * 🎨 chore: update getServerNameFromMatrixId helper * 🧪 chore: added utils tests * 🧪 chore: add invitation tokens listing tests * 🎨 fix: skip calling onbind API when no pending invitations found
1 parent 3c5a5a4 commit 8fd581f

File tree

6 files changed

+182
-2
lines changed

6 files changed

+182
-2
lines changed

packages/matrix-identity-server/src/3pid/bind.ts

+69-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {
77
validateParameters,
88
type expressAppHandler
99
} from '@twake/utils'
10+
import { buildUrl, getServerNameFromMatrixId } from '../utils'
11+
import type { onBindRequestPayload } from '../types'
1012

1113
const clientSecretRe = /^[0-9a-zA-Z.=_-]{6,255}$/
1214
const mxidRe = /^@[0-9a-zA-Z._=-]+:[0-9a-zA-Z.-]+$/
@@ -78,7 +80,18 @@ const bind = <T extends string = never>(
7880
return
7981
}
8082

81-
// TODO : hook for any pending invite and call the onbind api : https://spec.matrix.org/v1.11/client-server-api/#room-aliases
83+
_onBind(
84+
idServer,
85+
(obj as RequestTokenArgs).mxid,
86+
rows[0].address as string,
87+
rows[0].medium as string
88+
)
89+
.then(() => {
90+
idServer.logger.info(`Finished server onbind for ${mxid}`)
91+
})
92+
.catch((err) => {
93+
idServer.logger.error('Error calling server onbind', err)
94+
})
8295

8396
idServer.db
8497
.get('keys', ['data'], { name: 'pepper' })
@@ -152,4 +165,59 @@ const bind = <T extends string = never>(
152165
}
153166
}
154167

168+
/**
169+
* Calls the Matrix server onbind hook
170+
* @summary spec: https://spec.matrix.org/v1.13/server-server-api/#third-party-invites
171+
*
172+
* @param {MatrixIdentityServer} idServer
173+
* @param {string} mxid
174+
* @param {string} address
175+
* @param {string} medium
176+
*/
177+
const _onBind = async <T extends string = never>(
178+
idServer: MatrixIdentityServer<T>,
179+
mxid: string,
180+
address: string,
181+
medium: string
182+
): Promise<void> => {
183+
try {
184+
const server = getServerNameFromMatrixId(mxid)
185+
const invitationTokens = await idServer.db.listInvitationTokens(address)
186+
187+
if (!invitationTokens || !invitationTokens.length) {
188+
idServer.logger.info(`No pending invitations found for ${address}`)
189+
console.info(`No pending invitations found for ${address}`)
190+
191+
return
192+
}
193+
194+
const invites = invitationTokens.map(({ data }) => data)
195+
196+
const response = await fetch(
197+
buildUrl(server, `/_matrix/federation/v1/3pid/onbind`),
198+
{
199+
method: 'POST',
200+
headers: {
201+
'Content-Type': 'application/json'
202+
},
203+
body: JSON.stringify({
204+
mxid,
205+
address,
206+
medium,
207+
invites
208+
} satisfies onBindRequestPayload)
209+
}
210+
)
211+
212+
if (response.status !== 200) {
213+
throw new Error(
214+
`Failed to call onbind hook, status code: ${response.status}`
215+
)
216+
}
217+
} catch (error) {
218+
console.error(`Failed to call onbind hook`, { error })
219+
idServer.logger.error('Error calling onbind hook', error)
220+
}
221+
}
222+
155223
export default bind

packages/matrix-identity-server/src/db/index.test.ts

+30
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import DefaultConfig from '../config.json'
55
import { type Config, type DbGetResult } from '../types'
66
import IdDb from './index'
77
import fs from 'fs'
8+
import { assert } from 'console'
89

910
const baseConf: Config = {
1011
...DefaultConfig,
@@ -1224,4 +1225,33 @@ describe('Id Server DB', () => {
12241225
})
12251226
.catch(done)
12261227
})
1228+
1229+
it('should list invitation tokens', async () => {
1230+
const idDb = new IdDb(baseConf, logger)
1231+
1232+
await idDb.ready
1233+
1234+
await idDb.createInvitationToken('[email protected]', { test: 'a' })
1235+
await idDb.createInvitationToken('+21652111333', { test: 'b' })
1236+
await idDb.createInvitationToken('[email protected]', { test: 'c' })
1237+
1238+
const tokens = await idDb.listInvitationTokens('[email protected]')
1239+
assert(tokens)
1240+
1241+
expect(tokens?.length).toBe(2)
1242+
})
1243+
1244+
it('should return an empty array of no invitation tokens were found', async () => {
1245+
const idDb = new IdDb(baseConf, logger)
1246+
1247+
await idDb.ready
1248+
1249+
await idDb.createInvitationToken('[email protected]', { test: 'a' })
1250+
1251+
const tokens = await idDb.listInvitationTokens('+21652111222')
1252+
1253+
assert(tokens)
1254+
1255+
expect(tokens?.length).toBe(0)
1256+
})
12271257
})

packages/matrix-identity-server/src/db/index.ts

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { generateKeyPair, randomString } from '@twake/crypto'
22
import { type TwakeLogger } from '@twake/logger'
3-
import { type Config, type DbGetResult } from '../types'
3+
import { type invitationToken, type Config, type DbGetResult } from '../types'
44
import { epoch } from '@twake/utils'
55
import Pg from './sql/pg'
66
import { type ISQLCondition } from './sql/sql'
@@ -581,6 +581,35 @@ class IdentityServerDb<T extends string = never>
581581
})
582582
}
583583

584+
/**
585+
* list invitation tokens
586+
*
587+
* @param {string} address - the invited 3pid address
588+
* @returns {Promise<invitationToken[]>} - list of invitation tokens
589+
*/
590+
async listInvitationTokens(
591+
address: string
592+
): Promise<invitationToken[] | undefined> {
593+
if (this.db == null) {
594+
throw new Error('Wait for database to be ready')
595+
}
596+
597+
try {
598+
const tokenResults = await this.db.get(
599+
'invitationTokens',
600+
['data', 'address', 'id'],
601+
{ address }
602+
)
603+
return tokenResults.map((row) => JSON.parse(row.data as string))
604+
} catch (error) {
605+
console.error(`Failed to list invitation tokens for address`, {
606+
error
607+
})
608+
609+
this.logger.error('Failed to get tokens', error)
610+
}
611+
}
612+
584613
// eslint-disable-next-line @typescript-eslint/promise-function-async
585614
verifyInvitationToken(id: string): Promise<object> {
586615
/* istanbul ignore if */

packages/matrix-identity-server/src/types.ts

+32
Original file line numberDiff line numberDiff line change
@@ -89,3 +89,35 @@ export interface UserQuota {
8989
export interface ISMSService {
9090
send: (to: string, body: string) => Promise<void>
9191
}
92+
93+
export interface IdentityServerDomainSignature {
94+
'ed25519:0': string
95+
}
96+
97+
export interface IdServerSignature {
98+
mxid: string
99+
signatures: Record<string, IdentityServerDomainSignature>
100+
token: string
101+
}
102+
103+
export interface ThirPartyInvitePayload {
104+
address: string
105+
medium: string
106+
mxid: string
107+
room_id: string
108+
sender: string
109+
signed: IdServerSignature
110+
}
111+
112+
export interface onBindRequestPayload {
113+
address: string
114+
invites: ThirPartyInvitePayload[]
115+
medium: string
116+
mxid: string
117+
}
118+
119+
export interface invitationToken {
120+
id: string
121+
address: string
122+
data: ThirPartyInvitePayload
123+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { getServerNameFromMatrixId } from './utils'
2+
3+
describe('the getServerNameFromMatrixId helper', () => {
4+
it('should extract the server name correctly', () => {
5+
const firstServer = getServerNameFromMatrixId('@test:example.com')
6+
const secondServer = getServerNameFromMatrixId('@support:linagora.com')
7+
8+
expect(firstServer).toEqual('example.com')
9+
expect(secondServer).toEqual('linagora.com')
10+
})
11+
})

packages/matrix-identity-server/src/utils.ts

+10
Original file line numberDiff line numberDiff line change
@@ -120,3 +120,13 @@ export const buildUrl = (base: string, path: string): string => {
120120

121121
return finalUrl.toString()
122122
}
123+
124+
/**
125+
* Extracts the server name from a Matrix ID
126+
*
127+
* @param {string} mxid
128+
* @return {string}
129+
*/
130+
export const getServerNameFromMatrixId = (mxid: string): string => {
131+
return mxid.split(':')[1]
132+
}

0 commit comments

Comments
 (0)