From 90e0fa6fd0ce978da915752c36a21bff8b13a4fb Mon Sep 17 00:00:00 2001 From: h1pp0X Date: Mon, 8 Jul 2024 17:11:46 +0400 Subject: [PATCH] feat : added getEventId api --- .../src/__testData__/buildUserDB.ts | 8 +- .../matrix-client-server/src/index.test.ts | 200 ++++++++++++++++-- packages/matrix-client-server/src/index.ts | 10 +- .../src/matrixDb/index.ts | 135 +++++++++++- .../src/rooms/{roomId}/getEventId.ts | 104 +++++++++ packages/matrix-client-server/src/types.ts | 71 +++++++ 6 files changed, 502 insertions(+), 26 deletions(-) create mode 100644 packages/matrix-client-server/src/rooms/{roomId}/getEventId.ts diff --git a/packages/matrix-client-server/src/__testData__/buildUserDB.ts b/packages/matrix-client-server/src/__testData__/buildUserDB.ts index 29b63cbd..e8f8abfc 100644 --- a/packages/matrix-client-server/src/__testData__/buildUserDB.ts +++ b/packages/matrix-client-server/src/__testData__/buildUserDB.ts @@ -19,9 +19,11 @@ const insertQuery2 = const matrixDbQueries = [ 'CREATE TABLE IF NOT EXISTS profiles( user_id TEXT NOT NULL, displayname TEXT, avatar_url TEXT, UNIQUE(user_id) )', 'CREATE TABLE IF NOT EXISTS users( name TEXT, password_hash TEXT, creation_ts BIGINT, admin SMALLINT DEFAULT 0 NOT NULL, upgrade_ts BIGINT, is_guest SMALLINT DEFAULT 0 NOT NULL, appservice_id TEXT, consent_version TEXT, consent_server_notice_sent TEXT, user_type TEXT DEFAULT NULL, deactivated SMALLINT DEFAULT 0 NOT NULL, shadow_banned INT DEFAULT 0, consent_ts bigint, UNIQUE(name) )', - 'CREATE TABLE user_ips ( user_id TEXT NOT NULL, access_token TEXT NOT NULL, device_id TEXT, ip TEXT NOT NULL, user_agent TEXT NOT NULL, last_seen BIGINT NOT NULL)', - 'CREATE TABLE registration_tokens (token TEXT NOT NULL, uses_allowed INT, pending INT NOT NULL, completed INT NOT NULL, expiry_time BIGINT,UNIQUE (token))', - 'CREATE TABLE IF NOT EXISTS "devices" (user_id TEXT NOT NULL, device_id TEXT NOT NULL, display_name TEXT, last_seen BIGINT, ip TEXT, user_agent TEXT, hidden BOOLEAN DEFAULT 0,CONSTRAINT device_uniqueness UNIQUE (user_id, device_id))' + 'CREATE TABLE IF NOT EXISTS user_ips ( user_id TEXT NOT NULL, access_token TEXT NOT NULL, device_id TEXT, ip TEXT NOT NULL, user_agent TEXT NOT NULL, last_seen BIGINT NOT NULL)', + 'CREATE TABLE IF NOT EXISTS registration_tokens (token TEXT NOT NULL, uses_allowed INT, pending INT NOT NULL, completed INT NOT NULL, expiry_time BIGINT,UNIQUE (token))', + 'CREATE TABLE IF NOT EXISTS "devices" (user_id TEXT NOT NULL, device_id TEXT NOT NULL, display_name TEXT, last_seen BIGINT, ip TEXT, user_agent TEXT, hidden BOOLEAN DEFAULT 0,CONSTRAINT device_uniqueness UNIQUE (user_id, device_id))', + 'CREATE TABLE IF NOT EXISTS events( stream_ordering INTEGER PRIMARY KEY, topological_ordering BIGINT NOT NULL, event_id TEXT NOT NULL, type TEXT NOT NULL, room_id TEXT NOT NULL, content TEXT, unrecognized_keys TEXT, processed BOOL NOT NULL, outlier BOOL NOT NULL, depth BIGINT DEFAULT 0 NOT NULL, origin_server_ts BIGINT, received_ts BIGINT, sender TEXT, contains_url BOOLEAN, instance_name TEXT, state_key TEXT DEFAULT NULL, rejection_reason TEXT DEFAULT NULL, UNIQUE (event_id) )', + 'CREATE TABLE IF NOT EXISTS room_memberships( event_id TEXT NOT NULL, user_id TEXT NOT NULL, sender TEXT NOT NULL, room_id TEXT NOT NULL, membership TEXT NOT NULL, forgotten INTEGER DEFAULT 0, display_name TEXT, avatar_url TEXT, UNIQUE (event_id) )' ] // eslint-disable-next-line @typescript-eslint/promise-function-async diff --git a/packages/matrix-client-server/src/index.test.ts b/packages/matrix-client-server/src/index.test.ts index 2778535b..2508a949 100644 --- a/packages/matrix-client-server/src/index.test.ts +++ b/packages/matrix-client-server/src/index.test.ts @@ -20,7 +20,6 @@ jest.mock('nodemailer', () => ({ let conf: Config let clientServer: ClientServer let app: express.Application -let validToken: string const logger: TwakeLogger = getLogger() @@ -263,7 +262,45 @@ describe('Use configuration file', () => { }) }) + let validToken: string + let validToken2: string + let validToken3: string describe('Endpoints with authentication', () => { + beforeAll(async () => { + validToken = randomString(64) + validToken2 = randomString(64) + validToken3 = randomString(64) + try { + await clientServer.matrixDb.insert('user_ips', { + user_id: '@testuser:example.com', + device_id: 'testdevice', + access_token: validToken, + ip: '127.0.0.1', + user_agent: 'curl/7.31.0-DEV', + last_seen: 1411996332123 + }) + + await clientServer.matrixDb.insert('user_ips', { + user_id: '@testuser2:example.com', + device_id: 'testdevice2', + access_token: validToken2, + ip: '137.0.0.1', + user_agent: 'curl/7.31.0-DEV', + last_seen: 1411996332123 + }) + + await clientServer.matrixDb.insert('user_ips', { + user_id: '@testuser3:example.com', + device_id: 'testdevice3', + access_token: validToken3, + ip: '147.0.0.1', + user_agent: 'curl/7.31.0-DEV', + last_seen: 1411996332123 + }) + } catch (e) { + logger.error('Error creating tokens for authentification', e) + } + }) describe('/_matrix/client/v3/account/whoami', () => { it('should reject missing token (', async () => { const response = await request(app) @@ -286,15 +323,6 @@ describe('Use configuration file', () => { expect(response.statusCode).toBe(401) }) it('should accept valid token', async () => { - validToken = randomString(64) - await clientServer.matrixDb.insert('user_ips', { - user_id: '@testuser:example.com', - device_id: 'testdevice', - access_token: validToken, - ip: '127.0.0.1', - user_agent: 'curl/7.31.0-DEV', - last_seen: 1411996332123 - }) await clientServer.matrixDb.insert('users', { name: '@testuser:example.com', password_hash: 'hashedpassword', @@ -366,11 +394,11 @@ describe('Use configuration file', () => { ]) }) it('should work if the user has multiple devices and with multiple sessions', async () => { - const validToken2 = randomString(64) + const validTokenbis = randomString(64) await clientServer.matrixDb.insert('user_ips', { user_id: '@testuser:example.com', device_id: 'testdevice2', - access_token: validToken2, + access_token: validTokenbis, ip: '127.0.0.1', last_seen: 1411996332123, user_agent: 'curl/7.31.0-DEV' @@ -948,5 +976,153 @@ describe('Use configuration file', () => { }) }) }) + + describe('/_matrix/client/v3/rooms', () => { + describe('/_matrix/client/v3/rooms/{roomId}', () => { + describe('/_matrix/client/v3/rooms/{roomId}/event/{eventId}', () => { + beforeAll(async () => { + try { + await clientServer.matrixDb.insert('events', { + event_id: 'event_to_retrieve', + room_id: '!testroom:example.com', + sender: '@sender:example.com', + type: 'm.room.message', + state_key: '', + origin_server_ts: 1000, + content: '{ body: test message }', + topological_ordering: 0, + processed: 1, + outlier: 0 + }) + + await clientServer.matrixDb.insert('room_memberships', { + room_id: '!testroom:example.com', + user_id: '@testuser:example.com', + membership: 'join', + event_id: 'adding_user', + sender: '@admin:example.com' + }) + + await clientServer.matrixDb.insert('events', { + event_id: 'adding_user', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + type: 'm.room.message', + origin_server_ts: 0, + content: JSON.stringify({ body: 'test message' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + + logger.info('Test event created') + } catch (e) { + logger.error('Error setting up test data:', e) + } + }) + + afterAll(async () => { + try { + await clientServer.matrixDb.deleteEqual( + 'events', + 'event_id', + 'event_to_retrieve' + ) + await clientServer.matrixDb.deleteEqual( + 'events', + 'event_id', + 'adding_user' + ) + await clientServer.matrixDb.deleteEqual( + 'room_memberships', + 'event_id', + 'adding_user' + ) + logger.info('Test event deleted') + } catch (e) { + logger.error('Error tearing down test data', e) + } + }) + it('should return 404 if the event does not exist', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/event/invalid_event_id' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + }) + it('should 404 if the user has never been in the room', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/event/event_to_retrieve' + ) + .set('Authorization', `Bearer ${validToken2}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + }) + it('should return 200 if the event can be retrieved by the user', async () => { + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/event/event_to_retrieve' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(200) + expect(response.body).toHaveProperty( + 'event_id', + 'event_to_retrieve' + ) + expect(response.body).toHaveProperty( + 'room_id', + '!testroom:example.com' + ) + expect(response.body).toHaveProperty( + 'sender', + '@sender:example.com' + ) + expect(response.body).toHaveProperty('type', 'm.room.message') + expect(response.body).toHaveProperty('origin_server_ts', 1000) + expect(response.body).toHaveProperty( + 'content', + '{ body: test message }' + ) + }) + it('should return 404 if the user was not in the room at the time of the event', async () => { + try { + await clientServer.matrixDb.insert('room_memberships', { + room_id: '!testroom:example.com', + user_id: '@testuser:example.com', + membership: 'leave', + event_id: 'deleting_user', + sender: '@admin:example.com' + }) + + await clientServer.matrixDb.insert('events', { + event_id: 'deleting_user', + room_id: '!testroom:example.com', + sender: '@admin:example.com', + type: 'm.room.message', + origin_server_ts: 50, + content: JSON.stringify({ body: 'test message' }), + topological_ordering: 0, + processed: 2, + outlier: 0 + }) + logger.info('Test event created') + } catch (e) { + logger.error('Error tearing down test data', e) + } + const response = await request(app) + .get( + '/_matrix/client/v3/rooms/!testroom:example.com/event/event_to_retrieve' + ) + .set('Authorization', `Bearer ${validToken}`) + .set('Accept', 'application/json') + expect(response.statusCode).toBe(404) + }) + }) + }) + }) }) }) diff --git a/packages/matrix-client-server/src/index.ts b/packages/matrix-client-server/src/index.ts index 560dd9d2..5813c572 100644 --- a/packages/matrix-client-server/src/index.ts +++ b/packages/matrix-client-server/src/index.ts @@ -30,6 +30,7 @@ import whois from './admin/whois' import register from './register' import { getDevices, getDeviceInfo } from './devices/getDevices' import { changeDeviceName } from './devices/changeDevices' +import GetEventId from './rooms/{roomId}/getEventId' const tables = { ui_auth_sessions: 'session_id TEXT NOT NULL, stage_type TEXT NOT NULL' @@ -109,7 +110,8 @@ export default class MatrixClientServer extends MatrixIdentityServer> + fields: string[], + filterFields: Record>, + order?: string +) => Promise +type Get2 = ( + table: Collections, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string +) => Promise +type GetJoin = ( + tables: Array, + fields: string[], + filterFields: Record>, + joinFields: Record, + order?: string +) => Promise +type GetMax = ( + table: Collections, + targetField: string, + fields: string[], + filterFields: Record>, + order?: string +) => Promise +type GetMaxJoin2 = ( + tables: Array, + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + joinFields: Record, + order?: string ) => Promise type GetAll = (table: Collections, fields: string[]) => Promise @@ -52,6 +82,11 @@ type DeleteEqual = ( export interface MatrixDBmodifiedBackend { ready: Promise get: Get + getJoin: GetJoin + getWhereEqualOrDifferent: Get2 + getWhereEqualAndHigher: Get2 + getMaxWhereEqual: GetMax + getMaxWhereEqualAndLowerJoin: GetMaxJoin2 getAll: GetAll insert: Insert deleteEqual: DeleteEqual @@ -98,14 +133,98 @@ class MatrixDBmodified implements MatrixDBmodifiedBackend { return this.db.getAll(table, fields) } - get = async ( + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + get( table: Collections, - fields?: string[], - filterFields?: Record> - ): Promise => { - return await this.db.get(table, fields, filterFields) + fields: string[], + filterFields: Record>, + order?: string + ) { + return this.db.get(table, fields, filterFields, order) } + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getJoin( + table: Array, + fields: string[], + filterFields: Record>, + joinFields: Record, + order?: string + ) { + return this.db.getJoin(table, fields, filterFields, joinFields, order) + } + + //eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getWhereEqualOrDifferent( + table: Collections, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string + ) { + return this.db.getWhereEqualOrDifferent( + table, + fields, + filterFields1, + filterFields2, + order + ) + } + + //eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getWhereEqualAndHigher( + table: Collections, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + order?: string + ) { + return this.db.getWhereEqualAndHigher( + table, + fields, + filterFields1, + filterFields2, + order + ) + } + + //eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMaxWhereEqual( + table: Collections, + targetField: string, + fields: string[], + filterFields: Record>, + order?: string + ) { + return this.db.getMaxWhereEqual( + table, + targetField, + fields, + filterFields, + order + ) + } + + //eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async + getMaxWhereEqualAndLowerJoin( + tables: Array, + targetField: string, + fields: string[], + filterFields1: Record>, + filterFields2: Record>, + joinFields: Record, + order?: string + ) { + return this.db.getMaxWhereEqualAndLowerJoin( + tables, + targetField, + fields, + filterFields1, + filterFields2, + joinFields, + order + ) + } // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/promise-function-async insert(table: Collections, values: Record) { return this.db.insert(table, values) diff --git a/packages/matrix-client-server/src/rooms/{roomId}/getEventId.ts b/packages/matrix-client-server/src/rooms/{roomId}/getEventId.ts new file mode 100644 index 00000000..b644729d --- /dev/null +++ b/packages/matrix-client-server/src/rooms/{roomId}/getEventId.ts @@ -0,0 +1,104 @@ +import MatrixClientServer from '../..' +import { epoch, errMsg, send, type expressAppHandler } from '@twake/utils' +import { type ClientEvent } from '../../types' + +interface parameters { + eventId: string + roomId: string +} + +const GetEventId = (ClientServer: MatrixClientServer): expressAppHandler => { + return (req, res) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + const prms: parameters = (req as Request).params as parameters + ClientServer.authenticate(req, res, (data, id) => { + ClientServer.matrixDb + .get( + 'events', + [ + 'content', + 'event_id', + 'origin_server_ts', + 'room_id', + 'sender', + 'state_key', + 'type' + ], + { + event_id: prms.eventId, + room_id: prms.roomId + } + ) + .then((rows) => { + if (rows.length === 0) { + /* istanbul ignore next */ + ClientServer.logger.error('Event not found') + send(res, 404, errMsg('notFound', 'Cannot retrieve event')) + return + } + // Check if the user has permission to retrieve this event + const userId = data.sub as string + ClientServer.matrixDb + .getMaxWhereEqualAndLowerJoin( + ['room_memberships', 'events'], + 'events.origin_server_ts', + ['room_memberships.membership'], + { + 'room_memberships.user_id': userId, + 'room_memberships.room_id': prms.roomId + }, + { + 'events.origin_server_ts': rows[0].origin_server_ts + }, + { + 'room_memberships.event_id': 'events.event_id' + } + ) + .then((rows2) => { + if ( + rows2.length === 0 || + rows2[0].room_memberships_membership !== 'join' + ) { + /* istanbul ignore next */ + ClientServer.logger.error( + 'User not in the room at the time of the event' + ) + send(res, 404, errMsg('notFound', 'Cannot retrieve event')) + return + } + const event = rows[0] + const response = { + content: event.content, + event_id: event.event_id, + origin_server_ts: event.origin_server_ts, + room_id: event.room_id, + sender: event.sender, + type: event.type, + unsigned: { + age: epoch() - (event.origin_server_ts as number) + } + } as ClientEvent + if (event.state_key !== null) { + response.state_key = event.state_key as string + } + send(res, 200, response) + }) + .catch((err) => { + /* istanbul ignore next */ + ClientServer.logger.error(err) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', err)) + }) + }) + .catch((err) => { + /* istanbul ignore next */ + ClientServer.logger.error(err) + /* istanbul ignore next */ + send(res, 500, errMsg('unknown', err)) + }) + }) + } +} + +export default GetEventId diff --git a/packages/matrix-client-server/src/types.ts b/packages/matrix-client-server/src/types.ts index fef2e4d1..08c37eab 100644 --- a/packages/matrix-client-server/src/types.ts +++ b/packages/matrix-client-server/src/types.ts @@ -15,6 +15,77 @@ export type DbGetResult = Array< Record> > +export interface ClientEvent { + content: { [key: string]: any } + event_id: string + origin_server_ts: number + room_id: string + sender: string + state_key?: string + type: string + unsigned?: UnsignedData +} +export interface EventContent { + avatar_url?: string + displayname?: string | null + is_direct?: boolean + join_authorised_via_users_server?: boolean + membership: string + reason?: string + third_party_invite?: { [key: string]: any } +} + +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 LocalMediaRepository { + media_id: string + media_length: string + user_id: string +} +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 + prev_content?: { [key: string]: any } + redacted_because?: ClientEvent + transaction_id?: string +} + export interface LocalMediaRepository { media_id: string media_length: string