diff --git a/packages/matrix-client-server/src/__testData__/buildUserDB.ts b/packages/matrix-client-server/src/__testData__/buildUserDB.ts index 0a63258a..3db38db2 100644 --- a/packages/matrix-client-server/src/__testData__/buildUserDB.ts +++ b/packages/matrix-client-server/src/__testData__/buildUserDB.ts @@ -41,7 +41,8 @@ const matrixDbQueries = [ 'CREATE TABLE IF NOT EXISTS threepid_validation_token (token TEXT PRIMARY KEY,session_id TEXT NOT NULL,next_link TEXT,expires BIGINT NOT NULL)', 'CREATE TABLE IF NOT EXISTS presence (user_id TEXT NOT NULL, state VARCHAR(20), status_msg TEXT, mtime BIGINT, UNIQUE (user_id))', 'CREATE TABLE IF NOT EXISTS refresh_tokens (id BIGINT PRIMARY KEY,user_id TEXT NOT NULL,device_id TEXT NOT NULL,token TEXT NOT NULL,next_token_id BIGINT REFERENCES refresh_tokens (id) ON DELETE CASCADE, expiry_ts BIGINT DEFAULT NULL, ultimate_session_expiry_ts BIGINT DEFAULT NULL,UNIQUE(token))', - 'CREATE TABLE IF NOT EXISTS "access_tokens" (id BIGINT PRIMARY KEY, user_id TEXT NOT NULL, device_id TEXT, token TEXT NOT NULL,valid_until_ms BIGINT,puppets_user_id TEXT,last_validated BIGINT, refresh_token_id BIGINT REFERENCES refresh_tokens (id) ON DELETE CASCADE, used INT, UNIQUE(token))' + 'CREATE TABLE IF NOT EXISTS "access_tokens" (id BIGINT PRIMARY KEY, user_id TEXT NOT NULL, device_id TEXT, token TEXT NOT NULL,valid_until_ms BIGINT,puppets_user_id TEXT,last_validated BIGINT, refresh_token_id BIGINT REFERENCES refresh_tokens (id) ON DELETE CASCADE, used INT, UNIQUE(token))', + 'CREATE TABLE IF NOT EXISTS "user_filters" ( user_id TEXT NOT NULL, filter_id BIGINT NOT NULL, filter_json BYTEA NOT NULL )' ] // eslint-disable-next-line @typescript-eslint/promise-function-async diff --git a/packages/matrix-client-server/src/__testData__/loginConf.json b/packages/matrix-client-server/src/__testData__/loginConf.json deleted file mode 100644 index 0cc1c4a5..00000000 --- a/packages/matrix-client-server/src/__testData__/loginConf.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "cron_service": false, - "database_engine": "sqlite", - "database_host": "./src/__testData__/testLogin.db", - "matrix_database_engine": "sqlite", - "matrix_database_host": "./src/__testData__/testMatrixLogin.db", - "database_vacuum_delay": 7200, - "invitation_server_name": "matrix.to", - "is_federated_identity_service": false, - "key_delay": 3600, - "keys_depth": 5, - "mail_link_delay": 7200, - "rate_limiting_window": 10000, - "server_name": "matrix.org", - "smtp_sender": "yadd@debian.org", - "smtp_server": "localhost", - "template_dir": "./templates", - "userdb_engine": "sqlite", - "userdb_host": "./src/__testData__/testLogin.db", - "login_flows": { - "flows": [ - { - "type": "m.login.password" - }, - { - "get_login_token": true, - "type": "m.login.token" - } - ] - }, - "application_services": [ - { - "id": "test", - "hs_token": "hsTokenTestwdakZQunWWNe3DZitAerw9aNqJ2a6HVp0sJtg7qTJWXcHnBjgN0NL", - "as_token": "as_token_test", - "url": "http://localhost:3000", - "sender_localpart": "sender_localpart_test", - "namespaces": { - "users": [ - { - "exclusive": false, - "regex": "@_irc_bridge_.*" - } - ] - } - } - ], - "sms_folder": "./src/__testData__/sms", - "is_registration_enabled": true -} diff --git a/packages/matrix-client-server/src/__testData__/matrixDbTestConf.json b/packages/matrix-client-server/src/__testData__/matrixDbTestConf.json deleted file mode 100644 index 822b0ad1..00000000 --- a/packages/matrix-client-server/src/__testData__/matrixDbTestConf.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "base_url": "http://example.com/", - "cron_service": false, - "database_engine": "sqlite", - "database_host": "./src/__testData__/test.db", - "database_vacuum_delay": 7200, - "invitation_server_name": "matrix.to", - "is_federated_identity_service": false, - "key_delay": 3600, - "keys_depth": 5, - "mail_link_delay": 7200, - "rate_limiting_window": 10000, - "server_name": "matrix.org", - "smtp_sender": "yadd@debian.org", - "smtp_server": "localhost", - "template_dir": "./templates", - "userdb_engine": "sqlite", - "userdb_host": "./src/__testData__/test.db", - "matrix_database_engine": "sqlite", - "matrix_database_host": "./src/__testData__/testMatrix.db", - "login_flows": { - "flows": [ - { - "type": "m.login.password" - }, - { - "get_login_token": true, - "type": "m.login.token" - } - ] - }, - "application_services": [ - { - "id": "test", - "hs_token": "hsTokenTestwdakZQunWWNe3DZitAerw9aNqJ2a6HVp0sJtg7qTJWXcHnBjgN0NL", - "as_token": "as_token_test", - "url": "http://localhost:3000", - "sender_localpart": "sender_localpart_test", - "namespaces": { - "users": [ - { - "exclusive": false, - "regex": "@_irc_bridge_.*" - } - ] - } - } - ], - "sms_folder": "./src/__testData__/sms", - "is_registration_enabled": true -} diff --git a/packages/matrix-client-server/src/__testData__/presenceConf.json b/packages/matrix-client-server/src/__testData__/presenceConf.json deleted file mode 100644 index 922892d3..00000000 --- a/packages/matrix-client-server/src/__testData__/presenceConf.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "cron_service": false, - "database_engine": "sqlite", - "database_host": "./src/__testData__/testPresence.db", - "matrix_database_engine": "sqlite", - "matrix_database_host": "./src/__testData__/testMatrixPresence.db", - "database_vacuum_delay": 7200, - "invitation_server_name": "matrix.to", - "is_federated_identity_service": false, - "key_delay": 3600, - "keys_depth": 5, - "mail_link_delay": 7200, - "rate_limiting_window": 10000, - "server_name": "matrix.org", - "smtp_sender": "yadd@debian.org", - "smtp_server": "localhost", - "template_dir": "./templates", - "userdb_engine": "sqlite", - "userdb_host": "./src/__testData__/testPresence.db", - "login_flows": { - "flows": [ - { - "type": "m.login.password" - }, - { - "get_login_token": true, - "type": "m.login.token" - } - ] - }, - "application_services": [ - { - "id": "test", - "hs_token": "hsTokenTestwdakZQunWWNe3DZitAerw9aNqJ2a6HVp0sJtg7qTJWXcHnBjgN0NL", - "as_token": "as_token_test", - "url": "http://localhost:3000", - "sender_localpart": "sender_localpart_test", - "namespaces": { - "users": [ - { - "exclusive": false, - "regex": "@_irc_bridge_.*" - } - ] - } - } - ], - "sms_folder": "./src/__testData__/sms", - "is_registration_enabled": false -} diff --git a/packages/matrix-client-server/src/__testData__/registerConfRoom.json b/packages/matrix-client-server/src/__testData__/registerConfRoom.json deleted file mode 100644 index 6ac0d8ec..00000000 --- a/packages/matrix-client-server/src/__testData__/registerConfRoom.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "cron_service": false, - "database_engine": "sqlite", - "database_host": "./src/__testData__/testRoom.db", - "matrix_database_engine": "sqlite", - "matrix_database_host": "./src/__testData__/testMatrixRoom.db", - "database_vacuum_delay": 7200, - "invitation_server_name": "matrix.to", - "is_federated_identity_service": false, - "key_delay": 3600, - "keys_depth": 5, - "mail_link_delay": 7200, - "rate_limiting_window": 10000, - "server_name": "matrix.org", - "smtp_sender": "yadd@debian.org", - "smtp_server": "localhost", - "template_dir": "./templates", - "userdb_engine": "sqlite", - "userdb_host": "./src/__testData__/testRoom.db", - - "login_flows": { - "flows": [ - { - "type": "m.login.password" - }, - { - "get_login_token": true, - "type": "m.login.token" - } - ] - }, - "application_services": [ - { - "id": "test", - "hs_token": "hsTokenTestwdakZQunWWNe3DZitAerw9aNqJ2a6HVp0sJtg7qTJWXcHnBjgN0NL", - "as_token": "as_token_test", - "url": "http://localhost:3000", - "sender_localpart": "sender_localpart_test", - "namespaces": { - "users": [ - { - "exclusive": false, - "regex": "@_irc_bridge_.*" - } - ] - } - } - ], - "sms_folder": "./src/__testData__/sms", - "is_registration_enabled": true -} diff --git a/packages/matrix-client-server/src/__testData__/requestTokenConf.json b/packages/matrix-client-server/src/__testData__/requestTokenConf.json deleted file mode 100644 index 631520e4..00000000 --- a/packages/matrix-client-server/src/__testData__/requestTokenConf.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "cron_service": false, - "database_engine": "sqlite", - "database_host": "./src/__testData__/testRequestToken.db", - "matrix_database_engine": "sqlite", - "matrix_database_host": "./src/__testData__/testMatrixRequestToken.db", - "database_vacuum_delay": 7200, - "invitation_server_name": "matrix.to", - "is_federated_identity_service": false, - "key_delay": 3600, - "keys_depth": 5, - "mail_link_delay": 7200, - "rate_limiting_window": 10000, - "server_name": "matrix.org", - "smtp_sender": "yadd@debian.org", - "smtp_server": "localhost", - "template_dir": "./templates", - "userdb_engine": "sqlite", - "userdb_host": "./src/__testData__/testRequestToken.db", - - "login_flows": { - "flows": [ - { - "type": "m.login.password" - }, - { - "get_login_token": true, - "type": "m.login.token" - } - ] - }, - "application_services": [ - { - "id": "test", - "hs_token": "hsTokenTestwdakZQunWWNe3DZitAerw9aNqJ2a6HVp0sJtg7qTJWXcHnBjgN0NL", - "as_token": "as_token_test", - "url": "http://localhost:3000", - "sender_localpart": "sender_localpart_test", - "namespaces": { - "users": [ - { - "exclusive": false, - "regex": "@_irc_bridge_.*" - } - ] - } - } - ], - "sms_folder": "./src/__testData__/sms", - "is_registration_enabled": true -} diff --git a/packages/matrix-client-server/src/index.test.ts b/packages/matrix-client-server/src/index.test.ts index cad2188d..ac057b3c 100644 --- a/packages/matrix-client-server/src/index.test.ts +++ b/packages/matrix-client-server/src/index.test.ts @@ -34,11 +34,7 @@ beforeAll((done) => { // @ts-expect-error TS doesn't understand that the config is valid conf = { ...defaultConfig, - cron_service: false, - database_engine: 'sqlite', - base_url: 'http://example.com/', - userdb_engine: 'sqlite', - matrix_database_engine: 'sqlite' + base_url: 'http://example.com/' } if (process.env.TEST_PG === 'yes') { conf.database_engine = 'pg' @@ -1746,150 +1742,5 @@ describe('Use configuration file', () => { }) }) }) - - describe('/_matrix/client/v3/rooms/:roomId/aliases', () => { - const testUserId = '@testuser:example.com' - const testRoomId = '!testroomid:example.com' - const worldReadableRoomId = '!worldreadable:example.com' - - beforeAll(async () => { - try { - // Insert test data for room aliases - await clientServer.matrixDb.insert('room_aliases', { - room_id: testRoomId, - room_alias: '#somewhere:example.com' - }) - await clientServer.matrixDb.insert('room_aliases', { - room_id: testRoomId, - room_alias: '#another:example.com' - }) - await clientServer.matrixDb.insert('room_aliases', { - room_id: worldReadableRoomId, - room_alias: '#worldreadable:example.com' - }) - - // Insert test data for room visibility - await clientServer.matrixDb.insert('room_stats_state', { - room_id: worldReadableRoomId, - history_visibility: 'world_readable' - }) - await clientServer.matrixDb.insert('room_stats_state', { - room_id: testRoomId, - history_visibility: 'joined' - }) - - // Insert test data for room membership - await clientServer.matrixDb.insert('room_memberships', { - user_id: testUserId, - room_id: testRoomId, - membership: 'join', - forgotten: 0, - event_id: randomString(20), - sender: '@admin:example.com' - }) - } catch (e) { - logger.error('Error setting up test data:', e) - } - }) - - afterAll(async () => { - try { - // Clean up test data - await clientServer.matrixDb.deleteEqual( - 'room_aliases', - 'room_id', - testRoomId - ) - await clientServer.matrixDb.deleteEqual( - 'room_aliases', - 'room_id', - worldReadableRoomId - ) - await clientServer.matrixDb.deleteEqual( - 'room_stats_state', - 'room_id', - worldReadableRoomId - ) - await clientServer.matrixDb.deleteEqual( - 'room_stats_state', - 'room_id', - testRoomId - ) - await clientServer.matrixDb.deleteEqual( - 'room_memberships', - 'room_id', - testRoomId - ) - } catch (e) { - logger.error('Error tearing down test data:', e) - } - }) - - it('should require authentication', async () => { - const response = await request(app) - .get(`/_matrix/client/v3/rooms/${testRoomId}/aliases`) - .set('Authorization', 'Bearer invalidToken') - .set('Accept', 'application/json') - expect(response.statusCode).toBe(401) - }) - - it('should return 400 if the room ID is invalid', async () => { - const response = await request(app) - .get(`/_matrix/client/v3/rooms/invalid_room_id/aliases`) - .set('Authorization', `Bearer ${validToken2}`) - .set('Accept', 'application/json') - expect(response.statusCode).toBe(400) - expect(response.body.errcode).toEqual('M_INVALID_PARAM') - }) - - it('should return the list of aliases for a world_readable room for any user', async () => { - const response = await request(app) - .get(`/_matrix/client/v3/rooms/${worldReadableRoomId}/aliases`) - .set('Authorization', `Bearer ${validToken2}`) - .set('Accept', 'application/json') - expect(response.statusCode).toBe(200) - expect(response.body).toEqual({ - aliases: ['#worldreadable:example.com'] - }) - }) - - it('should return the list of aliases for an non-world_readable room if the user is a member', async () => { - const response = await request(app) - .get(`/_matrix/client/v3/rooms/${testRoomId}/aliases`) - .set('Authorization', `Bearer ${validToken}`) - .set('Accept', 'application/json') - expect(response.statusCode).toBe(200) - expect(response.body).toEqual({ - aliases: ['#somewhere:example.com', '#another:example.com'] - }) - }) - - it('should return 403 if the user is not a member and the room is not world_readable', async () => { - const response = await request(app) - .get(`/_matrix/client/v3/rooms/${testRoomId}/aliases`) - .set('Authorization', `Bearer ${validToken2}`) - .set('Accept', 'application/json') - expect(response.statusCode).toBe(403) - expect(response.body).toEqual({ - errcode: 'M_FORBIDDEN', - error: - 'The user is not permitted to retrieve the list of local aliases for the room' - }) - }) - - it('should return 400 if the room ID is invalid', async () => { - const invalidRoomId = '!invalidroomid:example.com' - - const response = await request(app) - .get(`/_matrix/client/v3/rooms/${invalidRoomId}/aliases`) - .set('Authorization', `Bearer ${validToken}`) - .set('Accept', 'application/json') - expect(response.statusCode).toBe(400) - expect(response.body).toEqual({ - errcode: 'M_INVALID_PARAM', - error: 'Invalid room id' - }) - }) - }) }) }) diff --git a/packages/matrix-client-server/src/index.ts b/packages/matrix-client-server/src/index.ts index 976409b1..35e1294b 100644 --- a/packages/matrix-client-server/src/index.ts +++ b/packages/matrix-client-server/src/index.ts @@ -57,6 +57,8 @@ import getStatus from './presence/getStatus' import putStatus from './presence/putStatus' import getLogin from './login/getLogin' import add from './account/3pid/add' +import PostFilter from './user/filter/postFilter' +import GetFilter from './user/filter/getFilter' import refresh from './refresh' const tables = { @@ -158,7 +160,9 @@ export default class MatrixClientServer extends MatrixIdentityServer jest.fn()) let conf: Config @@ -24,7 +24,10 @@ beforeAll((done) => { database_engine: 'sqlite', base_url: 'http://example.com/', userdb_engine: 'sqlite', - matrix_database_engine: 'sqlite' + matrix_database_engine: 'sqlite', + matrix_database_host: './src/__testData__/testMatrixLogin.db', + userdb_host: './src/__testData__/testLogin.db', + database_host: './src/__testData__/testLogin.db' } if (process.env.TEST_PG === 'yes') { conf.database_engine = 'pg' diff --git a/packages/matrix-client-server/src/matrixDb/index.test.ts b/packages/matrix-client-server/src/matrixDb/index.test.ts index b4c98842..937aa896 100644 --- a/packages/matrix-client-server/src/matrixDb/index.test.ts +++ b/packages/matrix-client-server/src/matrixDb/index.test.ts @@ -1,7 +1,7 @@ import MatrixDBmodified from './index' import { type TwakeLogger, getLogger } from '@twake/logger' import { type Config, type DbGetResult } from '../types' -import DefaultConfig from '../__testData__/matrixDbTestConf.json' +import DefaultConfig from '../__testData__/registerConf.json' import fs from 'fs' import { randomString } from '@twake/crypto' import { buildMatrixDb } from '../__testData__/buildUserDB' diff --git a/packages/matrix-client-server/src/matrixDb/index.ts b/packages/matrix-client-server/src/matrixDb/index.ts index e9e2f79c..1360a4ef 100644 --- a/packages/matrix-client-server/src/matrixDb/index.ts +++ b/packages/matrix-client-server/src/matrixDb/index.ts @@ -36,6 +36,7 @@ export type Collections = | 'presence' | 'access_tokens' | 'refresh_tokens' + | 'user_filters' type sqlComparaisonOperator = '=' | '!=' | '>' | '<' | '>=' | '<=' | '<>' interface ISQLCondition { diff --git a/packages/matrix-client-server/src/presence/presence.test.ts b/packages/matrix-client-server/src/presence/presence.test.ts index f8f81052..24ae30b1 100644 --- a/packages/matrix-client-server/src/presence/presence.test.ts +++ b/packages/matrix-client-server/src/presence/presence.test.ts @@ -4,11 +4,10 @@ import express from 'express' import ClientServer from '../index' import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' import { type Config } from '../types' -import defaultConfig from '../__testData__/presenceConf.json' +import defaultConfig from '../__testData__/registerConf.json' import { getLogger, type TwakeLogger } from '@twake/logger' import { setupTokens, validToken } from '../utils/setupTokens' -process.env.TWAKE_CLIENT_SERVER_CONF = './src/__testData__/presenceConf.json' jest.mock('node-fetch', () => jest.fn()) let conf: Config @@ -21,11 +20,11 @@ beforeAll((done) => { // @ts-expect-error TS doesn't understand that the config is valid conf = { ...defaultConfig, - cron_service: false, - database_engine: 'sqlite', base_url: 'http://example.com/', - userdb_engine: 'sqlite', - matrix_database_engine: 'sqlite' + matrix_database_host: './src/__testData__/testMatrixPresence.db', + userdb_host: './src/__testData__/testPresence.db', + database_host: './src/__testData__/testPresence.db', + is_registration_enabled: false } if (process.env.TEST_PG === 'yes') { conf.database_engine = 'pg' @@ -59,7 +58,7 @@ afterAll(() => { describe('Use configuration file', () => { beforeAll((done) => { - clientServer = new ClientServer() + clientServer = new ClientServer(conf) app = express() clientServer.ready .then(() => { diff --git a/packages/matrix-client-server/src/requestToken.test.ts b/packages/matrix-client-server/src/requestToken.test.ts index 4eba1a08..92f74e01 100644 --- a/packages/matrix-client-server/src/requestToken.test.ts +++ b/packages/matrix-client-server/src/requestToken.test.ts @@ -4,14 +4,11 @@ import express from 'express' import ClientServer from './index' import { buildMatrixDb, buildUserDB } from './__testData__/buildUserDB' import { type Config } from './types' -import defaultConfig from './__testData__/requestTokenConf.json' +import defaultConfig from './__testData__/registerConf.json' import { getLogger, type TwakeLogger } from '@twake/logger' import { epoch } from '@twake/utils' import { getSubmitUrl } from './register/email/requestToken' -process.env.TWAKE_CLIENT_SERVER_CONF = - './src/__testData__/requestTokenConf.json' - jest.mock('node-fetch', () => jest.fn()) const sendMailMock = jest.fn() jest.mock('nodemailer', () => ({ @@ -41,11 +38,10 @@ beforeAll((done) => { // @ts-expect-error TS doesn't understand that the config is valid conf = { ...defaultConfig, - cron_service: false, - database_engine: 'sqlite', base_url: 'http://example.com/', - userdb_engine: 'sqlite', - matrix_database_engine: 'sqlite' + matrix_database_host: 'src/__testData__/testMatrixRequestToken.db', + userdb_host: 'src/__testData__/testRequestToken.db', + database_host: 'src/__testData__/testRequestToken.db' } if (process.env.TEST_PG === 'yes') { conf.database_engine = 'pg' @@ -83,7 +79,7 @@ beforeEach(() => { describe('Use configuration file', () => { beforeAll((done) => { - clientServer = new ClientServer() + clientServer = new ClientServer(conf) app = express() clientServer.ready .then(() => { diff --git a/packages/matrix-client-server/src/rooms/rooms.test.ts b/packages/matrix-client-server/src/rooms/rooms.test.ts index e853a5da..59d51c5e 100644 --- a/packages/matrix-client-server/src/rooms/rooms.test.ts +++ b/packages/matrix-client-server/src/rooms/rooms.test.ts @@ -4,13 +4,11 @@ import express from 'express' import ClientServer from '../index' import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' import { type Config } from '../types' -import defaultConfig from '../__testData__/registerConfRoom.json' +import defaultConfig from '../__testData__/registerConf.json' import { getLogger, type TwakeLogger } from '@twake/logger' import { randomString } from '@twake/crypto' import { setupTokens, validToken, validToken2 } from '../utils/setupTokens' -process.env.TWAKE_CLIENT_SERVER_CONF = - './src/__testData__/registerConfRoom.json' jest.mock('node-fetch', () => jest.fn()) let conf: Config @@ -23,11 +21,10 @@ beforeAll((done) => { // @ts-expect-error TS doesn't understand that the config is valid conf = { ...defaultConfig, - cron_service: false, - database_engine: 'sqlite', base_url: 'http://example.com/', - userdb_engine: 'sqlite', - matrix_database_engine: 'sqlite' + matrix_database_host: './src/__testData__/testMatrixRoom.db', + userdb_host: './src/__testData__/testRoom.db', + database_host: './src/__testData__/testRoom.db' } if (process.env.TEST_PG === 'yes') { conf.database_engine = 'pg' @@ -61,7 +58,7 @@ afterAll(() => { describe('Use configuration file', () => { beforeAll((done) => { - clientServer = new ClientServer() + clientServer = new ClientServer(conf) app = express() clientServer.ready .then(() => { @@ -502,6 +499,142 @@ describe('Use configuration file', () => { }) }) }) + + describe('/_matrix/client/v3/rooms/:roomId/aliases', () => { + const testUserId = '@testuser:example.com' + const testRoomId = '!testroomid:example.com' + const worldReadableRoomId = '!worldreadable:example.com' + + beforeAll(async () => { + try { + // Insert test data for room aliases + await clientServer.matrixDb.insert('room_aliases', { + room_id: testRoomId, + room_alias: '#somewhere:example.com' + }) + await clientServer.matrixDb.insert('room_aliases', { + room_id: testRoomId, + room_alias: '#another:example.com' + }) + await clientServer.matrixDb.insert('room_aliases', { + room_id: worldReadableRoomId, + room_alias: '#worldreadable:example.com' + }) + + // Insert test data for room visibility + await clientServer.matrixDb.insert('room_stats_state', { + room_id: worldReadableRoomId, + history_visibility: 'world_readable' + }) + await clientServer.matrixDb.insert('room_stats_state', { + room_id: testRoomId, + history_visibility: 'joined' + }) + + // Insert test data for room membership + await clientServer.matrixDb.insert('room_memberships', { + user_id: testUserId, + room_id: testRoomId, + membership: 'join', + forgotten: 0, + event_id: randomString(20), + sender: '@admin:example.com' + }) + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + + afterAll(async () => { + try { + // Clean up test data + await clientServer.matrixDb.deleteEqual( + 'room_aliases', + 'room_id', + testRoomId + ) + await clientServer.matrixDb.deleteEqual( + 'room_aliases', + 'room_id', + worldReadableRoomId + ) + await clientServer.matrixDb.deleteEqual( + 'room_stats_state', + 'room_id', + worldReadableRoomId + ) + await clientServer.matrixDb.deleteEqual( + 'room_stats_state', + 'room_id', + testRoomId + ) + await clientServer.matrixDb.deleteEqual( + 'room_memberships', + 'room_id', + testRoomId + ) + } catch (e) { + logger.error('Error tearing down test data:', e) + } + }) + + it('should require authentication', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/rooms/${testRoomId}/aliases`) + .set('Authorization', 'Bearer invalidToken') + .set('Accept', 'application/json') + expect(response.statusCode).toBe(401) + }) + + it('should return the list of aliases for a world_readable room for any user', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/rooms/${worldReadableRoomId}/aliases`) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ + aliases: ['#worldreadable:example.com'] + }) + }) + + it('should return the list of aliases for an non-world_readable room if the user is a member', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/rooms/${testRoomId}/aliases`) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toEqual({ + aliases: ['#somewhere:example.com', '#another:example.com'] + }) + }) + + it('should return 403 if the user is not a member and the room is not world_readable', async () => { + const response = await request(app) + .get(`/_matrix/client/v3/rooms/${testRoomId}/aliases`) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toEqual({ + errcode: 'M_FORBIDDEN', + error: + 'The user is not permitted to retrieve the list of local aliases for the room' + }) + }) + + it('should return 400 if the room ID is invalid', async () => { + const invalidRoomId = '!invalidroomid:example.com' + + const response = await request(app) + .get(`/_matrix/client/v3/rooms/${invalidRoomId}/aliases`) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toEqual({ + errcode: 'M_INVALID_PARAM', + error: 'Invalid room id' + }) + }) + }) }) describe('/_matrix/client/v3/user/:userId/rooms/:roomId/tags', () => { diff --git a/packages/matrix-client-server/src/types.ts b/packages/matrix-client-server/src/types.ts index abfc2465..28d3c550 100644 --- a/packages/matrix-client-server/src/types.ts +++ b/packages/matrix-client-server/src/types.ts @@ -1,4 +1,5 @@ -// istanbul ignore file +/* istanbul ignore file */ + import { type IdentityServerDb, type Config as MIdentityServerConfig @@ -16,6 +17,55 @@ export type DbGetResult = Array< Record> > +/* +/* FILTERS */ +/* + +/* https://spec.matrix.org/latest/client-server-api/#post_matrixclientv3useruseridfilter */ +/* https://spec.matrix.org/latest/client-server-api/#get_matrixclientv3useruseridfilterfilterid */ + +export interface Filter { + account_data?: EventFilter + event_fields?: string[] + event_format?: string // 'client' | 'federation' + presence?: EventFilter + room?: RoomFilter +} + +export interface EventFilter { + limit?: number + not_senders?: string[] + not_types?: string[] + senders?: string[] + types?: string[] +} + +export interface RoomFilter { + account_data?: RoomEventFilter + ephemeral?: RoomEventFilter + include_leave?: boolean + not_rooms?: string[] + rooms?: string[] + state?: StateFilter + timeline?: RoomEventFilter +} + +export interface RoomEventFilter extends EventFilter { + contains_url?: boolean + include_redundant_members?: boolean + lazy_load_members?: boolean + not_rooms?: string[] + rooms?: string[] + unread_thread_notifications?: boolean +} + +export type StateFilter = RoomEventFilter + +/* +/* EVENTS */ +/* + +/* https://spec.matrix.org/latest/client-server-api/#room-event-format */ export interface ClientEvent { content: Record event_id: string @@ -26,27 +76,142 @@ export interface ClientEvent { type: string unsigned?: UnsignedData } + +export interface StateEvent extends Omit { + state_key: string +} + +/* https://spec.matrix.org/latest/client-server-api/#stripped-state */ +export interface StrippedStateEvent { + content: Record + sender: string + type: string + state_key: string +} + +export const stripEvent = (event: StateEvent): StrippedStateEvent => { + return { + content: event.content, + sender: event.sender, + type: event.type, + state_key: event.state_key + } +} + +/* ROOMS */ + +export interface RoomMember { + avatar_url: string + display_name: string +} +export interface PreviousRoom { + room_id: string + event_id: string +} + +/* +/* ROOM EVENTS */ +/* + +/* m.room.canonical_alias */ +/* https://spec.matrix.org/v1.11/client-server-api/#mroomcanonical_alias */ + +export interface RoomCanonicalAliasEvent extends StateEvent { + content: { + alias?: string + alt_aliases?: string[] + } + state_key: '' +} + +/* m.room.create */ +/* https://spec.matrix.org/v1.11/client-server-api/#mroomcreate */ +export interface RoomCreateEvent extends StateEvent { + content: { + creator?: string + 'm.federate'?: boolean + predecessor?: PreviousRoom + room_version?: string + type?: string + } + state_key: '' +} + +/* m.room.join_rules */ +/* https://spec.matrix.org/v1.11/client-server-api/#mroomjoin_rules */ +export interface RoomJoinRulesEvent extends StateEvent { + content: { + allow?: AllowCondition[] + join_rule: string + } + state_key: '' +} + +export interface AllowCondition { + room_id?: string + type: string // 'm.room_membership' +} + +/* m.room.member */ +/* https://spec.matrix.org/v1.11/client-server-api/#mroommember */ +export interface RoomMemberEvent extends StateEvent { + content: EventContent +} + export interface EventContent { avatar_url?: string displayname?: string | null is_direct?: boolean - join_authorised_via_users_server?: boolean + join_authorised_via_users_server?: string membership: string reason?: string - third_party_invite?: Record + third_party_invite?: Invite } -export interface EventFilter { - limit?: number - not_senders?: string[] - not_types?: string[] - senders?: string[] - types?: string[] -} export interface Invite { display_name: string signed: signed } + +export interface signed { + mxid: string + signatures: Record> + token: string +} + +export enum Membership { + INVITE = 'invite', + JOIN = 'join', + KNOCK = 'knock', + LEAVE = 'leave', + BAN = 'ban' +} + +/* m.room.power_levels */ +/* https://spec.matrix.org/v1.11/client-server-api/#mroompower_levels */ +export interface RoomPowerLevelsEvent extends StateEvent { + content: { + ban?: number + events?: Record + events_default?: number + invite?: number + kick?: number + notifications?: Notifications + redact?: number + state_default?: number + users?: Record + users_default?: number + } + state_key: '' +} + +export interface Notifications { + room?: number + [key: string]: number | undefined +} + +/* General */ + export interface LocalMediaRepository { media_id: string media_length: string @@ -55,30 +220,7 @@ export interface LocalMediaRepository { export interface MatrixUser { name: string } -export interface RoomEventFilter extends EventFilter { - contains_url?: boolean - include_redundant_members?: boolean - lazy_load_members?: boolean - unread_thread_notifications?: boolean -} -export interface RoomFilter { - account_data?: RoomEventFilter - ephemeral?: RoomEventFilter - include_leave?: boolean - not_rooms?: string[] - rooms?: string[] - state?: RoomEventFilter - timeline?: RoomEventFilter -} -export interface RoomMember { - avatar_url: string - display_name: string -} -export interface signed { - mxid: string - signatures: Record> - token: string -} + export interface UnsignedData { age?: number membership?: string @@ -96,8 +238,7 @@ export type clientDbCollections = 'ui_auth_sessions' export type ClientServerDb = IdentityServerDb -// Based on https://spec.matrix.org/v1.11/client-server-api/#identifier-types - +/* https://spec.matrix.org/v1.11/client-server-api/#identifier-types */ export interface MatrixIdentifier { type: 'm.id.user' user: string @@ -120,7 +261,7 @@ export type UserIdentifier = | ThirdPartyIdentifier | PhoneIdentifier -// Based on https://spec.matrix.org/v1.11/client-server-api/#authentication-types +/* https://spec.matrix.org/v1.11/client-server-api/#authentication-types */ export type AuthenticationTypes = | 'm.login.password' | 'm.login.email.identity' @@ -222,7 +363,7 @@ interface LoginFlow { type: AuthenticationTypes } -// https://spec.matrix.org/v1.11/application-service-api/#registration +/* https://spec.matrix.org/v1.11/application-service-api/#registration */ export interface AppServiceRegistration { as_token: string hs_token: string diff --git a/packages/matrix-client-server/src/user/filter/getFilter.ts b/packages/matrix-client-server/src/user/filter/getFilter.ts new file mode 100644 index 00000000..e776a357 --- /dev/null +++ b/packages/matrix-client-server/src/user/filter/getFilter.ts @@ -0,0 +1,43 @@ +import type MatrixClientServer from '../..' +import { errMsg, send, type expressAppHandler } from '@twake/utils' +import { type Request } from 'express' + +const GetFilter = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (data, id) => { + const filterId = (req as Request).params.filterId + const userId = (req as Request).params.userId + if (userId !== data.sub || !clientServer.isMine(userId)) { + clientServer.logger.error( + 'Forbidden user id for getting a filter:', + userId + ) + send(res, 403, errMsg('forbidden')) + return + } + clientServer.matrixDb + .get('user_filters', ['filter_json'], { + user_id: userId, + filter_id: filterId + }) + .then((rows) => { + if (rows.length === 0) { + clientServer.logger.error('Filter not found') + send(res, 404, errMsg('notFound', 'Cannot retrieve filter')) + return + } + const filter = JSON.parse(rows[0].filter_json as string) // TODO : clarify the type of the filter_json (bytea, string ???) + clientServer.logger.info('Fetched filter:', filterId) + send(res, 200, filter) + }) + .catch((e) => { + /* istanbul ignore next */ + clientServer.logger.error('Error while fetching filter', e) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown')) + }) + }) + } +} + +export default GetFilter diff --git a/packages/matrix-client-server/src/user/filter/postFilter.ts b/packages/matrix-client-server/src/user/filter/postFilter.ts new file mode 100644 index 00000000..d6e9299a --- /dev/null +++ b/packages/matrix-client-server/src/user/filter/postFilter.ts @@ -0,0 +1,73 @@ +import { + errMsg, + type expressAppHandler, + jsonContent, + send, + validateParametersStrict, + matrixIdRegex +} from '@twake/utils' +import type MatrixClientServer from '../..' +import type { Request } from 'express' +import { randomString } from '@twake/crypto' +import { Filter } from '../../utils/filter' + +const schema = { + account_data: false, + event_fields: false, + event_format: false, + presence: false, + room: false +} + +const PostFilter = (clientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + clientServer.authenticate(req, res, (token) => { + jsonContent(req, res, clientServer.logger, (obj) => { + validateParametersStrict( + res, + schema, + obj, + clientServer.logger, + (obj) => { + const filter: Filter = new Filter(obj) + // TODO : verify if the user is allowed to make requests for this user id + // we consider for the moment that the user is only allowed to make requests for his own user id + const userId = (req as Request).params.userId + if (!matrixIdRegex.test(userId)) { + send(res, 400, errMsg('invalidParam', 'Invalid user ID')) + return + } + if (userId !== token.sub || !clientServer.isMine(userId)) { + clientServer.logger.error( + 'Forbidden user id for posting a filter:', + userId + ) + send(res, 403, errMsg('forbidden')) + return + } + // Assuming this will guarantee the unique constraint + const filterId = randomString(16) + clientServer.matrixDb + .insert('user_filters', { + user_id: userId, + filter_id: filterId, + filter_json: JSON.stringify(filter) // TODO : clarify the type of the filter_json (bytea, string ???) + }) + .then(() => { + clientServer.logger.info('Inserted filter:', filterId) + send(res, 200, { filter_id: filterId }) + }) + .catch((e) => { + /* istanbul ignore next */ + clientServer.logger.error('Error while inserting filter:', e) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', e)) + }) + } + ) + }) + }) + } +} + +export default PostFilter diff --git a/packages/matrix-client-server/src/user/user.test.ts b/packages/matrix-client-server/src/user/user.test.ts new file mode 100644 index 00000000..c2bbb53f --- /dev/null +++ b/packages/matrix-client-server/src/user/user.test.ts @@ -0,0 +1,563 @@ +import fs from 'fs' +import request from 'supertest' +import express from 'express' +import ClientServer from '../index' +import { buildMatrixDb, buildUserDB } from '../__testData__/buildUserDB' +import { type Config, type Filter } from '../types' +import defaultConfig from '../__testData__/registerConf.json' +import { getLogger, type TwakeLogger } from '@twake/logger' +import { setupTokens, validToken } from '../utils/setupTokens' + +jest.mock('node-fetch', () => jest.fn()) +const sendMailMock = jest.fn() +jest.mock('nodemailer', () => ({ + createTransport: jest.fn().mockImplementation(() => ({ + sendMail: sendMailMock + })) +})) + +let conf: Config +let clientServer: ClientServer +let app: express.Application + +const logger: TwakeLogger = getLogger() + +beforeAll((done) => { + // @ts-expect-error TS doesn't understand that the config is valid + conf = { + ...defaultConfig, + base_url: 'http://example.com/', + matrix_database_host: 'src/__testData__/userTestMatrix.db', + userdb_host: 'src/__testData__/userTest.db', + database_host: 'src/__testData__/userTest.db' + } + if (process.env.TEST_PG === 'yes') { + conf.database_engine = 'pg' + conf.userdb_engine = 'pg' + conf.database_host = process.env.PG_HOST ?? 'localhost' + conf.database_user = process.env.PG_USER ?? 'twake' + conf.database_password = process.env.PG_PASSWORD ?? 'twake' + conf.database_name = process.env.PG_DATABASE ?? 'test' + } + buildUserDB(conf) + .then(() => { + buildMatrixDb(conf) + .then(() => { + done() + }) + .catch((e) => { + logger.error('Error while building matrix db:', e) + done(e) + }) + }) + .catch((e) => { + logger.error('Error while building user db:', e) + done(e) + }) +}) + +afterAll(() => { + fs.unlinkSync('src/__testData__/userTest.db') + fs.unlinkSync('src/__testData__/userTestMatrix.db') +}) + +beforeEach(() => { + jest.clearAllMocks() +}) + +describe('Use configuration file', () => { + beforeAll((done) => { + clientServer = new ClientServer(conf) + app = express() + clientServer.ready + .then(() => { + Object.keys(clientServer.api.get).forEach((k) => { + app.get(k, clientServer.api.get[k]) + }) + Object.keys(clientServer.api.post).forEach((k) => { + app.post(k, clientServer.api.post[k]) + }) + Object.keys(clientServer.api.put).forEach((k) => { + app.put(k, clientServer.api.put[k]) + }) + Object.keys(clientServer.api.delete).forEach((k) => { + app.delete(k, clientServer.api.delete[k]) + }) + done() + }) + .catch((e) => { + done(e) + }) + }) + + afterAll(() => { + clientServer.cleanJobs() + }) + + describe('Endpoints with authentication', () => { + beforeAll(async () => { + await setupTokens(clientServer, logger) + }) + + describe('/_matrix/client/v3/user/:userId', () => { + describe('/_matrix/client/v3/user/:userId/account_data/:type', () => { + it('should reject invalid userId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/invalidUserId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject invalid roomId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/invalidRoomId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body).toHaveProperty('errcode', 'M_NOT_FOUND') + }) + it('should refuse to return account data for another user', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@anotheruser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should return account data', async () => { + await clientServer.matrixDb.insert('account_data', { + user_id: '@testuser:example.com', + account_data_type: 'm.room.message', + stream_id: 1, + content: 'test content' + }) + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body['m.room.message']).toBe('test content') + }) + it('should reject invalid userId', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/invalidUserId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_UNKNOWN') // Error code from jsonContent function of @twake/utils + }) + it('should refuse to update account data for another user', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@anotheruser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'new content' }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should update account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'updated content' }) + expect(response.statusCode).toBe(200) + const response2 = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response2.statusCode).toBe(200) + expect(response2.body['m.room.message']).toBe('updated content') + }) + }) + + describe('/_matrix/client/v3/user/:userId/rooms/:roomId/account_data/:type', () => { + describe('GET', () => { + it('should reject invalid userId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/invalidUserId/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject invalid roomId', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/invalidRoomId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body).toHaveProperty('errcode', 'M_NOT_FOUND') + }) + it('should refuse to return account data for another user', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/user/@anotheruser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should return account data', async () => { + await clientServer.matrixDb.insert('room_account_data', { + user_id: '@testuser:example.com', + account_data_type: 'm.room.message', + stream_id: 1, + content: 'test content', + room_id: '!roomId:example.com' + }) + const response = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body['m.room.message']).toBe('test content') + }) + }) + describe('PUT', () => { + it('should reject invalid userId', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/invalidUserId/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject invalid roomId', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/invalidRoomId/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject an invalid event type', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/invalidEventType' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_INVALID_PARAM') + }) + it('should reject missing account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'M_UNKNOWN') // Error code from jsonContent function of @twake/utils + }) + it('should refuse to update account data for another user', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@anotheruser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'new content' }) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should update account data', async () => { + const response = await request(app) + .put( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ content: 'updated content' }) + expect(response.statusCode).toBe(200) + const response2 = await request(app) + .get( + '/_matrix/client/v3/user/@testuser:example.com/rooms/!roomId:example.com/account_data/m.room.message' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response2.statusCode).toBe(200) + expect(response2.body['m.room.message']).toBe('updated content') + }) + }) + }) + describe('/_matrix/client/v3/user/:userId/filter', () => { + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('user_filters', { + user_id: '@testuser:example.com', + filter_id: '1234', + filter_json: JSON.stringify({ filter: true }) + }) + await clientServer.matrixDb.insert('user_filters', { + user_id: '@testuser2:example.com', + filter_id: '1235', + filter_json: JSON.stringify({ filter: true }) + }) + await clientServer.matrixDb.insert('user_filters', { + user_id: '@testuser:example2.com', + filter_id: '1234', + filter_json: JSON.stringify({ filter: true }) + }) + logger.info('Filters inserted') + } catch (e) { + logger.error('Error inserting filters in db', e) + } + }) + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'user_filters', + 'user_id', + '@testuser:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'user_filters', + 'user_id', + '@testuser2:example.com' + ) + await clientServer.matrixDb.deleteEqual( + 'user_filters', + 'user_id', + '@testuser:example2.com' + ) + logger.info('Filters deleted') + } catch (e) { + logger.error('Error deleting filters in db', e) + } + }) + const filter: Filter = { + event_fields: ['type', 'content', 'sender'], + event_format: 'client', + presence: { + not_senders: ['@alice:example.com'], + types: ['m.presence'] + }, + room: { + ephemeral: { + not_rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'], + types: ['m.receipt', 'm.typing'] + }, + state: { + not_rooms: ['!726s6s6q:example.com'], + types: ['m.room.*'] + }, + timeline: { + limit: 10, + not_rooms: ['!726s6s6q:example.com'], + not_senders: ['@spam:example.com'], + types: ['m.room.message'] + } + } + } + let filterId: string + + describe('POST', () => { + it('should reject invalid parameters', async () => { + // Additional parameters not supported + const response = await request(app) + .post('/_matrix/client/v3/user/@testuser:example.com/filter') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send({ notAFilterField: 'test' }) + expect(response.statusCode).toBe(400) + expect(response.body).toHaveProperty('errcode', 'UNKNOWN_PARAM') + }) + it('should reject posting a filter for an other userId', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user/@testuser2:example.com/filter') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send(filter) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should reject posting a filter for an other server name', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user/@testuser:example2.com/filter') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send(filter) + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should post a filter', async () => { + const response = await request(app) + .post('/_matrix/client/v3/user/@testuser:example.com/filter') + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + .send(filter) + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty('filter_id') + filterId = response.body.filter_id + }) + }) + describe('GET', () => { + it('should reject getting a filter for an other userId', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/@testuser2:example.com/filter/${filterId}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should reject getting a filter for an other server name', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/@testuser:example2.com/filter/${filterId}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(403) + expect(response.body).toHaveProperty('errcode', 'M_FORBIDDEN') + }) + it('should reject getting a filter that does not exist', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/@testuser:example.com/filter/invalidFilterId` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + expect(response.body).toHaveProperty('errcode', 'M_NOT_FOUND') + }) + it('should get a filter', async () => { + const response = await request(app) + .get( + `/_matrix/client/v3/user/@testuser:example.com/filter/${filterId}` + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + // We can't simply write expect(response.body).toEqual(filter) because many default values were added + expect(response.body.event_fields).toEqual(filter.event_fields) + expect(response.body.event_format).toEqual(filter.event_format) + expect(response.body.presence.not_senders).toEqual( + filter.presence?.not_senders + ) + expect(response.body.presence.types).toEqual(filter.presence?.types) + expect(response.body.room.ephemeral.not_rooms).toEqual( + filter.room?.ephemeral?.not_rooms + ) + expect(response.body.room.ephemeral.not_senders).toEqual( + filter.room?.ephemeral?.not_senders + ) + expect(response.body.room.ephemeral.types).toEqual( + filter.room?.ephemeral?.types + ) + expect(response.body.room.state.not_rooms).toEqual( + filter.room?.state?.not_rooms + ) + expect(response.body.room.state.types).toEqual( + filter.room?.state?.types + ) + expect(response.body.room.timeline.limit).toEqual( + filter.room?.timeline?.limit + ) + expect(response.body.room.timeline.not_rooms).toEqual( + filter.room?.timeline?.not_rooms + ) + expect(response.body.room.timeline.not_senders).toEqual( + filter.room?.timeline?.not_senders + ) + expect(response.body.room.timeline.types).toEqual( + filter.room?.timeline?.types + ) + }) + }) + }) + }) + }) +}) diff --git a/packages/matrix-identity-server/src/db/sql/_createTables.ts b/packages/matrix-identity-server/src/db/sql/_createTables.ts index 02ce5ff4..79fa8e80 100644 --- a/packages/matrix-identity-server/src/db/sql/_createTables.ts +++ b/packages/matrix-identity-server/src/db/sql/_createTables.ts @@ -19,7 +19,9 @@ function createTables( .then((count) => { /* istanbul ignore else */ // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions if (!count) { - db.rawQuery(`CREATE TABLE ${table}(${tables[table]})`) + db.rawQuery( + `CREATE TABLE IF NOT EXISTS ${table}(${tables[table]})` + ) // eslint-disable-next-line @typescript-eslint/promise-function-async .then(() => Promise.all( @@ -32,7 +34,7 @@ function createTables( >((index) => db .rawQuery( - `CREATE INDEX i_${table}_${index} ON ${table} (${index})` + `CREATE INDEX IF NOT EXISTS i_${table}_${index} ON ${table} (${index})` ) .catch((e) => { /* istanbul ignore next */ diff --git a/packages/utils/src/size_limits.md b/packages/utils/src/size_limits.md new file mode 100644 index 00000000..29667c06 --- /dev/null +++ b/packages/utils/src/size_limits.md @@ -0,0 +1,15 @@ +## General Constraints + +- The complete event MUST NOT be larger than 65536 bytes when formatted with the federation event format, including any signatures, and encoded as Canonical JSON. + +## Size Restrictions Per Key + +| Key | Maximum Size | +|------------|------------------------------------------------------------------| +| sender | 255 bytes (including the `@` sigil and the domain) | +| room_id | 255 bytes (including the `!` sigil and the domain) | +| state_key | 255 bytes | +| type | 255 bytes | +| event_id | 255 bytes (including the `$` sigil and the domain where present) | + +Some event types have additional size restrictions which are specified in the description of the event. Additional restrictions exist also for specific room versions. Additional keys have no limit other than that implied by the total 64 KiB limit on events. diff --git a/packages/utils/src/utils.ts b/packages/utils/src/utils.ts index 0efb9dc8..6f73b2cb 100644 --- a/packages/utils/src/utils.ts +++ b/packages/utils/src/utils.ts @@ -124,9 +124,7 @@ const _validateParameters: validateParametersType = ( } }) if (additionalParameters.length > 0) { - if (acceptAdditionalParameters == null || acceptAdditionalParameters) { - logger.warn('Additional parameters', additionalParameters) - } else { + if (acceptAdditionalParameters === false) { logger.error('Additional parameters', additionalParameters) send( res, @@ -136,9 +134,13 @@ const _validateParameters: validateParametersType = ( `Unknown additional parameters ${additionalParameters.join(', ')}` ) ) + } else { + logger.warn('Additional parameters', additionalParameters) + callback(content) } + } else { + callback(content) } - callback(content) } }