From d2543dca2f2d45aac5302835c8f970d7e12b5e8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Tue, 12 Sep 2023 15:02:00 +0200 Subject: [PATCH 01/16] Storage: add function to get mapping by matrix ID --- src/helpers/storage.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/helpers/storage.ts b/src/helpers/storage.ts index df99642..a3031dd 100644 --- a/src/helpers/storage.ts +++ b/src/helpers/storage.ts @@ -25,6 +25,12 @@ export function getMapping( }) } +export function getMappingByMatrixId(id: string): Promise { + return AppDataSource.manager.findOneBy(IdMapping, { + matrixId: id, + }) +} + export async function save(entity: IdMapping | Membership): Promise { await AppDataSource.manager.save(entity) } From a7afe7b5700607dbb7a0d84034fee2c3f24e1afc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Tue, 12 Sep 2023 15:05:39 +0200 Subject: [PATCH 02/16] Add members to channel if they posted a message --- src/handlers/messages.ts | 80 +++++++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 9 deletions(-) diff --git a/src/handlers/messages.ts b/src/handlers/messages.ts index 3180ea6..6f8b4ae 100644 --- a/src/handlers/messages.ts +++ b/src/handlers/messages.ts @@ -1,8 +1,17 @@ +import { AxiosError } from 'axios' import { Entity, entities } from '../Entities' import { IdMapping } from '../entity/IdMapping' import log from '../helpers/logger' -import { getMessageId, getRoomId, getUserId, save } from '../helpers/storage' +import { + getMapping, + getMappingByMatrixId, + getMessageId, + getRoomId, + getUserId, + save, +} from '../helpers/storage' import { axios, formatUserSessionOptions } from '../helpers/synapse' +import { acceptInvitation, inviteMember } from './rooms' const applicationServiceToken = process.env.AS_TOKEN || '' if (!applicationServiceToken) { @@ -134,13 +143,66 @@ export async function handle(rcMessage: RcMessage): Promise { } } - const event_id = await createMessage( - matrixMessage, - room_id, - user_id, - ts, - rcMessage._id - ) + try { + const event_id = await createMessage( + matrixMessage, + room_id, + user_id, + ts, + rcMessage._id + ) + createMapping(rcMessage._id, event_id) + } catch (error) { + if ( + error instanceof AxiosError && + error.response && + error.response.data.errcode === 'M_FORBIDDEN' && + error.response.data.error === `User ${user_id} not in room ${room_id}` + ) { + log.info(error.response.data.error + ', adding.') - createMapping(rcMessage._id, event_id) + const userMapping = await getMapping( + rcMessage.u._id, + entities[Entity.Users].mappingType + ) + if (!userMapping || !userMapping.matrixId || !userMapping.accessToken) { + log.warn(`Could not determine joining user, skipping.`, rcMessage) + return + } + + // Get room creator session or use empty axios options + let userSessionOptions = {} + const roomCreatorId = ( + await axios.get(`/_synapse/admin/v1/rooms/${room_id}`) + ).data.creator + if (!roomCreatorId) { + log.warn( + `Could not determine room creator for room ${room_id}, using admin credentials.` + ) + } else { + const creatorMapping = await getMappingByMatrixId(roomCreatorId) + if (!creatorMapping?.accessToken) { + log.warn(`Could not access token for ${roomCreatorId}, skipping.`) + return + } + userSessionOptions = formatUserSessionOptions( + creatorMapping.accessToken + ) + } + + await inviteMember(userMapping.matrixId, room_id, userSessionOptions) + await acceptInvitation(userMapping, room_id) + + const event_id = await createMessage( + matrixMessage, + room_id, + user_id, + ts, + rcMessage._id + ) + createMapping(rcMessage._id, event_id) + } else { + throw error + } + } } From c4e7659e30f9e160c3a999a19116710d61ea8455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Tue, 12 Sep 2023 15:06:10 +0200 Subject: [PATCH 03/16] Add event type handling --- src/handlers/messages.ts | 70 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/src/handlers/messages.ts b/src/handlers/messages.ts index 6f8b4ae..5ca612c 100644 --- a/src/handlers/messages.ts +++ b/src/handlers/messages.ts @@ -102,11 +102,6 @@ export async function handle(rcMessage: RcMessage): Promise { return } - if (rcMessage.t) { - log.warn(`Message ${rcMessage._id} is of type ${rcMessage.t}, skipping.`) - return - } - const room_id = await getRoomId(rcMessage.rid) if (!room_id) { log.warn( @@ -115,6 +110,71 @@ export async function handle(rcMessage: RcMessage): Promise { return } + if (rcMessage.t) { + switch (rcMessage.t) { + case 'ru': // User removed by + case 'ul': // User left + case 'ult': // User left team + case 'removed-user-from-team': // Removed user from team + log.info( + `Message ${rcMessage._id} is of type ${rcMessage.t}, removing member ${rcMessage.msg} from room ${room_id}` + ) + + const members = ( + await axios.get( + `/_matrix/client/v3/rooms/${room_id}/joined_members`, + formatUserSessionOptions(applicationServiceToken) + ) + ).data.joined + if (!members) { + const errorMessage = `Could not determine members of room ${room_id}, aborting` + log.error(errorMessage) + throw new Error(errorMessage) + } + + const matrixUser = + Object.keys(members).find((key) => + key.includes(rcMessage.msg.toLowerCase()) + ) || '' + + const userMapping = await getMappingByMatrixId(matrixUser) + if (!userMapping?.accessToken) { + log.warn( + `Could not get access token for ${rcMessage.msg}, maybe user is not a member, skipping.` + ) + return + } + + log.http(`User ${matrixUser} leaves room ${room_id}`) + await axios.post( + `/_matrix/client/v3/rooms/${room_id}/leave`, + { reason: `Event type ${rcMessage.t}` }, + formatUserSessionOptions(userMapping.accessToken) + ) + return + + case 'uj': // User joined channel + case 'ujt': // User joined team + case 'ut': // User joined conversation + + case 'au': // User added by + case 'added-user-to-team': // Added user to team + case 'r': // Room name changed + case 'rm': // Message removed + log.info( + `Message ${rcMessage._id} is of type ${rcMessage.t}, for which Rocket.Chat does not provide the initial state information, skipping.` + ) + return + + case 'user-muted': // User muted by + default: + log.warn( + `Message ${rcMessage._id} is of unhandled type ${rcMessage.t}, skipping.` + ) + return + } + } + const user_id = await getUserId(rcMessage.u._id) if (!user_id) { log.warn( From 1746d6f31b12ae9a70934632d2ffede365702b17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Tue, 12 Sep 2023 15:18:57 +0200 Subject: [PATCH 04/16] Add comments to reset script --- reset.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/reset.sh b/reset.sh index 73e39f1..b714de7 100755 --- a/reset.sh +++ b/reset.sh @@ -4,12 +4,14 @@ IFS=$'\n\t' HOMESERVER="http://localhost:8008" +echo 'Resetting containers and databases' docker-compose down sudo rm -f files/homeserver.db rm -f db.sqlite docker-compose up -d sleep 1.5 +echo 'Creating admin user' set +e until docker-compose exec -it synapse register_new_matrix_user $HOMESERVER -c /data/homeserver.yaml --admin --user verdiadmin --password verdiadmin &> /dev/null do @@ -17,8 +19,11 @@ do done set -e +echo 'Saving admin access token' curl --request POST \ --url $HOMESERVER/_matrix/client/v3/login \ --header 'Content-Type: application/json' \ --data '{"type": "m.login.password","user": "verdiadmin","password": "verdiadmin","device_id": "DEV"}' \ > src/config/synapse_access_token.json 2> /dev/null + +echo 'Done.' From 29036deb58090831bd5babd5dabb69f7dc261bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Wed, 13 Sep 2023 16:41:41 +0200 Subject: [PATCH 05/16] Write logs to files --- src/helpers/logger.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts index 356139b..a3d0466 100644 --- a/src/helpers/logger.ts +++ b/src/helpers/logger.ts @@ -2,7 +2,11 @@ import winston from 'winston' export default winston.createLogger({ level: 'debug', - transports: [new winston.transports.Console()], + transports: [ + new winston.transports.Console(), + new winston.transports.File({ filename: 'warn.log', level: 'warn' }), + new winston.transports.File({ filename: 'combined.log' }), + ], format: winston.format.combine( winston.format.colorize({ all: true }), winston.format.simple() From 4fb240b39a245ce5b8372e44cf1f1ebdfb06c993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Wed, 13 Sep 2023 16:42:14 +0200 Subject: [PATCH 06/16] Use warning log level for skipped entities --- src/handlers/messages.ts | 2 +- src/handlers/rooms.test.ts | 4 +++- src/handlers/rooms.ts | 16 ++++++++++++---- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/handlers/messages.ts b/src/handlers/messages.ts index 5ca612c..999a512 100644 --- a/src/handlers/messages.ts +++ b/src/handlers/messages.ts @@ -161,7 +161,7 @@ export async function handle(rcMessage: RcMessage): Promise { case 'added-user-to-team': // Added user to team case 'r': // Room name changed case 'rm': // Message removed - log.info( + log.warn( `Message ${rcMessage._id} is of type ${rcMessage.t}, for which Rocket.Chat does not provide the initial state information, skipping.` ) return diff --git a/src/handlers/rooms.test.ts b/src/handlers/rooms.test.ts index d2f3fc6..1f0ef2e 100644 --- a/src/handlers/rooms.test.ts +++ b/src/handlers/rooms.test.ts @@ -106,7 +106,9 @@ test('mapping private rooms', () => { test('mapping live chats', () => { expect(() => mapRoom({ _id: 'liveChatId', t: RcRoomTypes.live }) - ).toThrowError('Room type l is unknown') + ).toThrowError( + 'Room with ID: liveChatId is a live chat. Migration not implemented' + ) }) test('getting creator', () => { diff --git a/src/handlers/rooms.ts b/src/handlers/rooms.ts index 9568372..d5b46e5 100644 --- a/src/handlers/rooms.ts +++ b/src/handlers/rooms.ts @@ -83,10 +83,18 @@ export function mapRoom(rcRoom: RcRoom): MatrixRoom { break case RcRoomTypes.live: + const messageLivechat = `Room ${ + rcRoom.name || 'with ID: ' + rcRoom._id + } is a live chat. Migration not implemented` + log.warn(messageLivechat) + throw new Error(messageLivechat) + default: - const message = `Room type ${rcRoom.t} is unknown or unimplemented` - log.error(message) - throw new Error(message) + const messageUnknownRoom = `Room ${ + rcRoom.name || 'with ID: ' + rcRoom._id + } is of type ${rcRoom.t}, which is unknown or unimplemented` + log.error(messageUnknownRoom) + throw new Error(messageUnknownRoom) } return room } @@ -98,7 +106,7 @@ export function getCreator(rcRoom: RcRoom): string { return rcRoom.uids[0] } else { log.warn( - `Creator ID could not be determined for room ${rcRoom.name} of type ${rcRoom.t}. This is normal for the default room.` + `Creator ID could not be determined for room ${rcRoom.name} of type ${rcRoom.t}. This is normal for the default room. Using admin user.` ) return '' } From 1aff3561ae6a81499b966de24e1090fc8b9526e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Wed, 13 Sep 2023 16:52:31 +0200 Subject: [PATCH 07/16] Fix test logs being written to file --- src/helpers/logger.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts index a3d0466..47fd52f 100644 --- a/src/helpers/logger.ts +++ b/src/helpers/logger.ts @@ -4,8 +4,15 @@ export default winston.createLogger({ level: 'debug', transports: [ new winston.transports.Console(), - new winston.transports.File({ filename: 'warn.log', level: 'warn' }), - new winston.transports.File({ filename: 'combined.log' }), + new winston.transports.File({ + filename: 'warn.log', + level: 'warn', + silent: process.env.NODE_ENV === 'test', + }), + new winston.transports.File({ + filename: 'combined.log', + silent: process.env.NODE_ENV === 'test', + }), ], format: winston.format.combine( winston.format.colorize({ all: true }), From 2b03b3eee589a375168a97f24813297d548ce174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Thu, 14 Sep 2023 16:05:12 +0200 Subject: [PATCH 08/16] Remove functions.js draft --- src/functions.js | 93 ------------------------------------------------ 1 file changed, 93 deletions(-) delete mode 100644 src/functions.js diff --git a/src/functions.js b/src/functions.js deleted file mode 100644 index 2a23890..0000000 --- a/src/functions.js +++ /dev/null @@ -1,93 +0,0 @@ -postToMatrix (endpoint, payload) {} -mapUserId (id) {} -mapChannelId (id) {} -mapMessageId (id) {} -generateHmac(user) {} - -mapRoom (rcRoom) { - const room = { - creation_content: { - 'm.federate': false - }, - name: rcRoom.name, - room_alias_name: rcRoom.name, - topic: rcRoom.description, - // TODO: Invite users (Rate Limit?) - // POST /_matrix/client/v3/rooms/{roomId}/invite - // { - // "reason": "Welcome to the team!", - // "user_id": "@cheeky_monkey:matrix.org" - // } - } - - switch (rcRoom.t) { - case 'd': - room.is_direct = true - break; - - case 'c': - room.preset = 'public_chat' - break; - - case 'p': - room.preset = 'private_chat' - break; - - default: - // log; 'l' for livechat, anything else is undefined - break; - } - // POST /_matrix/client/v3/createRoom -} - -mapUser (rcUser) { - return { - 'nonce': '', - 'username': rcUser.username, - 'displayname': rcUser.name, - 'password': '', - 'admin': rcUser.roles.includes('admin'), - 'mac': '', - } -} - -getUserRegisterNonce () {} // GET /_synapse/admin/v1/register - -createUser (rcUser) { - const user = mapUser(rcUser) - user.nonce = getUserRegisterNonce() - user.mac = generateHmac(user) - const mUser = postToMatrix('/_synapse/admin/v1/register', user) // POST /_synapse/admin/v1/register - - // rcUser.__rooms.map(mapChannelId) - return mUser -} - -mapMessage (rcMessage) { - const message = { - 'content': { - 'body': rc.msg, - // 'format': 'org.matrix.custom.html', - // 'formatted_body': 'This is an example text message', - 'msgtype': 'm.text', - }, - 'event_id': '$143273582443PhrSn:example.org', // TODO: ?? - 'origin_server_ts': new Date(rc.t.$date).valueOf(), - 'room_id': mapChannelId(rcMessage.rid), - 'sender': mapUserId(rc.u._id), - 'type': 'm.room.message', - 'unsigned': { - 'age': 1234, // TODO: ?? - }, - } - // TODO: Other media types - - if (rc.tmid) { // If it is a thread reply - message.content['m.relates_to'] = { - rel_type: 'm.thread', - event_id: mapMessageId(rc.tmid), - } - } - - return message -} From 07f41ec61839144410236bfd92f2b8ea1a464e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Thu, 14 Sep 2023 16:05:45 +0200 Subject: [PATCH 09/16] Remove logs during reset --- reset.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/reset.sh b/reset.sh index b714de7..3ee7b19 100755 --- a/reset.sh +++ b/reset.sh @@ -26,4 +26,7 @@ curl --request POST \ --data '{"type": "m.login.password","user": "verdiadmin","password": "verdiadmin","device_id": "DEV"}' \ > src/config/synapse_access_token.json 2> /dev/null +echo 'Removing log files' +rm ./*.log + echo 'Done.' From ba900e913f8d2a058d30cf03241436cd88c1e5d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Thu, 14 Sep 2023 16:09:34 +0200 Subject: [PATCH 10/16] Add function to get all mappings by type --- src/helpers/storage.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/helpers/storage.ts b/src/helpers/storage.ts index a3031dd..b2ddf2b 100644 --- a/src/helpers/storage.ts +++ b/src/helpers/storage.ts @@ -1,7 +1,7 @@ import { DataSource } from 'typeorm' +import { Entity, entities } from '../Entities' import { IdMapping } from '../entity/IdMapping' import { Membership } from '../entity/Membership' -import { Entity, entities } from '../Entities' const AppDataSource = new DataSource({ type: 'sqlite', @@ -25,6 +25,10 @@ export function getMapping( }) } +export function getAllMappingsByType(type: number): Promise { + return AppDataSource.manager.findBy(IdMapping, { type }) +} + export function getMappingByMatrixId(id: string): Promise { return AppDataSource.manager.findOneBy(IdMapping, { matrixId: id, From 86e938794b6f70a12490605869dc54b664b1ecce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Thu, 14 Sep 2023 16:12:04 +0200 Subject: [PATCH 11/16] Add exception handling for already joined users --- src/handlers/rooms.test.ts | 4 ++-- src/handlers/rooms.ts | 26 +++++++++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/handlers/rooms.test.ts b/src/handlers/rooms.test.ts index 1f0ef2e..2df7082 100644 --- a/src/handlers/rooms.test.ts +++ b/src/handlers/rooms.test.ts @@ -1,5 +1,6 @@ import { expect, jest, test } from '@jest/globals' import axios from 'axios' +import { Entity, entities } from '../Entities' import { IdMapping } from '../entity/IdMapping' import * as storage from '../helpers/storage' import { SessionOptions } from '../helpers/synapse' @@ -9,15 +10,14 @@ import { RcRoom, RcRoomTypes, acceptInvitation, + createDirectChatMemberships, createMapping, getCreator, getFilteredMembers, inviteMember, mapRoom, - createDirectChatMemberships, registerRoom, } from './rooms' -import { Entity, entities } from '../Entities' jest.mock('axios') const mockedAxios = axios as jest.Mocked diff --git a/src/handlers/rooms.ts b/src/handlers/rooms.ts index d5b46e5..574c327 100644 --- a/src/handlers/rooms.ts +++ b/src/handlers/rooms.ts @@ -1,3 +1,4 @@ +import { AxiosError } from 'axios' import { Entity, entities } from '../Entities' import { IdMapping } from '../entity/IdMapping' import log from '../helpers/logger' @@ -160,11 +161,26 @@ export async function inviteMember( creatorSessionOptions: SessionOptions | object ): Promise { log.http(`Invite member ${inviteeId}`) - await axios.post( - `/_matrix/client/v3/rooms/${roomId}/invite`, - { user_id: inviteeId }, - creatorSessionOptions - ) + try { + await axios.post( + `/_matrix/client/v3/rooms/${roomId}/invite`, + { user_id: inviteeId }, + creatorSessionOptions + ) + } catch (error) { + if ( + error instanceof AxiosError && + error.response && + error.response.data.errcode === 'M_FORBIDDEN' && + error.response.data.error === `${inviteeId} is already in the room.` + ) { + log.debug( + `User ${inviteeId} is already in room ${roomId}, probably because this user created the room as a fallback.` + ) + } else { + throw error + } + } } export async function acceptInvitation( From c498193ee3051fd0cd486cff00ea76b7f1a43126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Thu, 14 Sep 2023 16:12:44 +0200 Subject: [PATCH 12/16] Add await for mapping creation --- src/handlers/messages.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/messages.ts b/src/handlers/messages.ts index 999a512..a068901 100644 --- a/src/handlers/messages.ts +++ b/src/handlers/messages.ts @@ -211,7 +211,7 @@ export async function handle(rcMessage: RcMessage): Promise { ts, rcMessage._id ) - createMapping(rcMessage._id, event_id) + await createMapping(rcMessage._id, event_id) } catch (error) { if ( error instanceof AxiosError && @@ -260,7 +260,7 @@ export async function handle(rcMessage: RcMessage): Promise { ts, rcMessage._id ) - createMapping(rcMessage._id, event_id) + await createMapping(rcMessage._id, event_id) } else { throw error } From 806d41cc091c9d35d464abc096b4eb4035195afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Thu, 14 Sep 2023 16:13:07 +0200 Subject: [PATCH 13/16] Sort imports --- src/helpers/storage.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/helpers/storage.test.ts b/src/helpers/storage.test.ts index b4de1cf..84f8dd3 100644 --- a/src/helpers/storage.test.ts +++ b/src/helpers/storage.test.ts @@ -1,5 +1,8 @@ process.env.DATABASE = ':memory:' import { beforeAll, expect, test } from '@jest/globals' +import { Entity, entities } from '../Entities' +import { IdMapping } from '../entity/IdMapping' +import { Membership } from '../entity/Membership' import { createMembership, getAccessToken, @@ -11,9 +14,6 @@ import { initStorage, save, } from './storage' -import { IdMapping } from '../entity/IdMapping' -import { Membership } from '../entity/Membership' -import { Entity, entities } from '../Entities' const mapping = new IdMapping() mapping.rcId = 'rcId' From b141488f16d4e72804b4432bb89a0bc67da2116c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Thu, 14 Sep 2023 16:14:22 +0200 Subject: [PATCH 14/16] Add creation of admin user mapping --- .env.example | 1 + src/handlers/users.test.ts | 7 ++++--- src/handlers/users.ts | 39 ++++++++++++++++++++++++-------------- 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/.env.example b/.env.example index 0142512..de2cf7a 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ REGISTRATION_SHARED_SECRET='look in your synapses homeserver.yaml' AS_TOKEN='look in the app-service.yaml' EXCLUDED_USERS='rocket.cat' # Comma-separated list of usernames or IDs +ADMIN_USERNAME='admin' # The login name of the Rocket.Chat admin diff --git a/src/handlers/users.test.ts b/src/handlers/users.test.ts index 612aad2..9e8d0af 100644 --- a/src/handlers/users.test.ts +++ b/src/handlers/users.test.ts @@ -1,8 +1,10 @@ process.env.REGISTRATION_SHARED_SECRET = 'ThisIsSoSecretWow' process.env.EXCLUDED_USERS = 'excludedUser1,excludedUser2' +process.env.ADMIN_USERNAME = 'testAdmin' import { expect, jest, test } from '@jest/globals' import axios from 'axios' -import * as storage from '../helpers/storage' +import { Entity, entities } from '../Entities' +import { IdMapping } from '../entity/IdMapping' import { MatrixUser, RcUser, @@ -12,8 +14,7 @@ import { mapUser, userIsExcluded, } from '../handlers/users' -import { IdMapping } from '../entity/IdMapping' -import { Entity, entities } from '../Entities' +import * as storage from '../helpers/storage' jest.mock('axios') const mockedAxios = axios as jest.Mocked diff --git a/src/handlers/users.ts b/src/handlers/users.ts index 9c3ae1c..be4f073 100644 --- a/src/handlers/users.ts +++ b/src/handlers/users.ts @@ -1,9 +1,10 @@ import { createHmac } from 'node:crypto' -import log from '../helpers/logger' -import { axios } from '../helpers/synapse' -import { createMembership, getUserId, save } from '../helpers/storage' -import { IdMapping } from '../entity/IdMapping' import { Entity, entities } from '../Entities' +import adminAccessToken from '../config/synapse_access_token.json' +import { IdMapping } from '../entity/IdMapping' +import log from '../helpers/logger' +import { createMembership, getUserId, save } from '../helpers/storage' +import { axios } from '../helpers/synapse' export type RcUser = { _id: string @@ -41,15 +42,22 @@ export function mapUser(rcUser: RcUser): MatrixUser { } } -const registration_shared_secret = process.env.REGISTRATION_SHARED_SECRET || '' -if (!registration_shared_secret) { +const registrationSharedSecret = process.env.REGISTRATION_SHARED_SECRET || '' +if (!registrationSharedSecret) { const message = 'No REGISTRATION_SHARED_SECRET found in .env.' log.error(message) throw new Error(message) } +const adminUsername = process.env.ADMIN_USERNAME || '' +if (!adminUsername) { + const message = 'No ADMIN_USERNAME found in .env.' + log.error(message) + throw new Error(message) +} + export function generateHmac(user: MatrixUser): string { - const hmac = createHmac('sha1', registration_shared_secret) + const hmac = createHmac('sha1', registrationSharedSecret) hmac.write( `${user.nonce}\0${user.username}\0${user.password}\0${ user.admin ? 'admin' : 'notadmin' @@ -87,7 +95,7 @@ export function userIsExcluded(rcUser: RcUser): boolean { reasons.push(`username "${rcUser.username}" is on exclusion list`) if (reasons.length > 0) { - log.debug(`User ${rcUser.name} is excluded: ${reasons.join(', ')}`) + log.warn(`User ${rcUser.name} is excluded: ${reasons.join(', ')}`) return true } return false @@ -124,15 +132,18 @@ export async function createUser(rcUser: RcUser): Promise { export async function handle(rcUser: RcUser): Promise { log.info(`Parsing user: ${rcUser.name}: ${rcUser._id}`) - if (userIsExcluded(rcUser)) { - return undefined - } - const matrixId = await getUserId(rcUser._id) if (matrixId) { log.debug(`Mapping exists: ${rcUser._id} -> ${matrixId}`) } else { - const matrixUser = await createUser(rcUser) - await createMapping(rcUser._id, matrixUser) + if (rcUser.username === adminUsername) { + log.info( + `User ${rcUser.username} is defined as admin in ENV, mapping as such` + ) + await createMapping(rcUser._id, adminAccessToken as unknown as MatrixUser) + } else if (!userIsExcluded(rcUser)) { + const matrixUser = await createUser(rcUser) + await createMapping(rcUser._id, matrixUser) + } } } From 9c9dd97f3376505a221c8193e19291646e43af4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Thu, 14 Sep 2023 16:25:17 +0200 Subject: [PATCH 15/16] Remove excess members from rooms --- src/app.ts | 76 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/src/app.ts b/src/app.ts index dc50312..2155df2 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,18 +1,26 @@ import dotenv from 'dotenv' dotenv.config() +import { AxiosError } from 'axios' import lineByLine from 'n-readlines' import 'reflect-metadata' -import { handle as handleRoom } from './handlers/rooms' -import { handle as handleUser } from './handlers/users' -import { handle as handleMessage } from './handlers/messages' -import log from './helpers/logger' -import { initStorage } from './helpers/storage' -import { whoami } from './helpers/synapse' import { Entity, entities } from './Entities' -import { AxiosError } from 'axios' +import { handle as handleMessage } from './handlers/messages' +import { getFilteredMembers, handle as handleRoom } from './handlers/rooms' +import { handle as handleUser } from './handlers/users' +import log from './helpers/logger' +import { + getAllMappingsByType, + getMappingByMatrixId, + getMemberships, + initStorage, +} from './helpers/storage' +import { axios, formatUserSessionOptions, whoami } from './helpers/synapse' + +const applicationServiceToken = process.env.AS_TOKEN || '' log.info('rocketchat2matrix starts.') +// eslint-disable-next-line @typescript-eslint/no-unused-vars async function loadRcExport(entity: Entity) { const rl = new lineByLine(`./inputs/${entities[entity].filename}`) @@ -38,6 +46,57 @@ async function loadRcExport(entity: Entity) { } } +async function removeExcessRoomMembers() { + const roomMappings = await getAllMappingsByType( + entities[Entity.Rooms].mappingType + ) + if (!roomMappings) { + throw new Error(`No room mappings found`) + } + + roomMappings.forEach(async (roomMapping) => { + log.info( + `Checking memberships for room ${roomMapping.rcId} / ${roomMapping.matrixId}:` + ) + // get all memberships from db + const rcMemberIds = await getMemberships(roomMapping.rcId) + const memberMappings = await getFilteredMembers(rcMemberIds, '') + const memberNames: string[] = memberMappings.map( + (memberMapping) => memberMapping.matrixId || '' + ) + // get each mx rooms' mx users + const actualMembers: string[] = Object.keys( + ( + await axios.get( + `/_matrix/client/v3/rooms/${roomMapping.matrixId}/joined_members`, + formatUserSessionOptions(applicationServiceToken) + ) + ).data.joined + ) + + // do action for any user in mx, but not in rc + await Promise.all( + actualMembers.map(async (actualMember) => { + if (!memberNames.includes(actualMember)) { + log.warn( + `Member ${actualMember} should not be in room ${roomMapping.matrixId}, removing` + ) + const memberMapping = await getMappingByMatrixId(actualMember) + if (!memberMapping || !memberMapping.accessToken) { + throw new Error(`Could not find access token for member ${actualMember}, this is a bug`) + } + + await axios.post( + `/_matrix/client/v3/rooms/${roomMapping.matrixId}/leave`, + {}, + formatUserSessionOptions(memberMapping.accessToken) + ) + } + }) + ) + }) +} + async function main() { try { await whoami() @@ -48,6 +107,9 @@ async function main() { await loadRcExport(Entity.Rooms) log.info('Parsing messages') await loadRcExport(Entity.Messages) + log.info('Checking room memberships') + await removeExcessRoomMembers() + log.info('Done.') } catch (error) { if (error instanceof AxiosError) { From a276d8f8a2722f9a49c8de2b3ea5ef6842004c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20H=C3=BCttemann?= Date: Thu, 14 Sep 2023 16:52:35 +0200 Subject: [PATCH 16/16] Overwrite log files --- src/app.ts | 4 +++- src/helpers/logger.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index 2155df2..50bdfa1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -83,7 +83,9 @@ async function removeExcessRoomMembers() { ) const memberMapping = await getMappingByMatrixId(actualMember) if (!memberMapping || !memberMapping.accessToken) { - throw new Error(`Could not find access token for member ${actualMember}, this is a bug`) + throw new Error( + `Could not find access token for member ${actualMember}, this is a bug` + ) } await axios.post( diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts index 47fd52f..72d3313 100644 --- a/src/helpers/logger.ts +++ b/src/helpers/logger.ts @@ -8,10 +8,12 @@ export default winston.createLogger({ filename: 'warn.log', level: 'warn', silent: process.env.NODE_ENV === 'test', + options: { flags: 'w' }, }), new winston.transports.File({ filename: 'combined.log', silent: process.env.NODE_ENV === 'test', + options: { flags: 'w' }, }), ], format: winston.format.combine(