snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
114
ee/packages/federation-matrix/src/events/edu.ts
Normal file
114
ee/packages/federation-matrix/src/events/edu.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
import { api, Room } from '@rocket.chat/core-services';
|
||||
import { UserStatus } from '@rocket.chat/core-typings';
|
||||
import { federationSDK } from '@rocket.chat/federation-sdk';
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { Messages, Rooms, Users } from '@rocket.chat/models';
|
||||
|
||||
const logger = new Logger('federation-matrix:edu');
|
||||
|
||||
export const edus = async () => {
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.typing', async (data) => {
|
||||
try {
|
||||
const matrixRoom = await Rooms.findOne({ 'federation.mrid': data.room_id }, { projection: { _id: 1 } });
|
||||
if (!matrixRoom) {
|
||||
logger.debug({ msg: 'No bridged room found for Matrix room_id', roomId: data.room_id });
|
||||
return;
|
||||
}
|
||||
|
||||
void api.broadcast('user.activity', {
|
||||
user: data.user_id,
|
||||
isTyping: data.typing,
|
||||
roomId: matrixRoom._id,
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Error handling Matrix typing event', err });
|
||||
}
|
||||
});
|
||||
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.presence', async (data) => {
|
||||
try {
|
||||
const matrixUser = await Users.findOneByUsername(data.user_id);
|
||||
if (!matrixUser) {
|
||||
logger.debug({ msg: 'No federated user found for Matrix user_id', userId: data.user_id });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!matrixUser.federated) {
|
||||
logger.debug({ msg: 'User is not federated, skipping presence update from Matrix', username: matrixUser.username });
|
||||
return;
|
||||
}
|
||||
|
||||
const statusMap = {
|
||||
online: UserStatus.ONLINE,
|
||||
offline: UserStatus.OFFLINE,
|
||||
unavailable: UserStatus.AWAY,
|
||||
};
|
||||
|
||||
const status = statusMap[data.presence] || UserStatus.OFFLINE;
|
||||
|
||||
if (matrixUser.status === status) {
|
||||
logger.debug({ msg: 'User already has target status, skipping update', username: matrixUser.username, status });
|
||||
return;
|
||||
}
|
||||
|
||||
await Users.updateOne(
|
||||
{ _id: matrixUser._id },
|
||||
{
|
||||
$set: {
|
||||
status,
|
||||
statusDefault: status,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const { _id, username, statusText, roles, name } = matrixUser;
|
||||
void api.broadcast('presence.status', {
|
||||
user: { status, _id, username, statusText, roles, name },
|
||||
previousStatus: undefined,
|
||||
});
|
||||
logger.debug({ msg: 'Updated presence for user from Matrix federation', userId: matrixUser._id, status });
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Error handling Matrix presence event', err });
|
||||
}
|
||||
});
|
||||
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.receipt', async (data) => {
|
||||
try {
|
||||
const matrixUser = await Users.findOneByUsername(data.user_id);
|
||||
if (!matrixUser) {
|
||||
logger.debug({ msg: 'No federated user found for Matrix user_id', userId: data.user_id });
|
||||
return;
|
||||
}
|
||||
|
||||
const matrixRoom = await Rooms.findOne({ 'federation.mrid': data.room_id });
|
||||
if (!matrixRoom) {
|
||||
logger.debug({ msg: 'No bridged room found for Matrix room_id', roomId: data.room_id });
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.thread_id && data.thread_id !== 'main') {
|
||||
const msg = await Messages.findOneByFederationId(data.thread_id);
|
||||
if (!msg) {
|
||||
logger.debug({ msg: 'No message found for Matrix thread_id', threadId: data.thread_id });
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.rid !== matrixRoom._id) {
|
||||
logger.warn({
|
||||
msg: 'Message thread_id does not belong to the expected room',
|
||||
threadId: data.thread_id,
|
||||
expectedRoomId: matrixRoom._id,
|
||||
actualRoomId: msg.rid,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await Room.readThread({ room: matrixRoom, user: matrixUser, tmid: msg._id });
|
||||
} else {
|
||||
await Room.markAsRead(matrixRoom, matrixUser._id);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Error handling Matrix receipt event', err });
|
||||
}
|
||||
});
|
||||
};
|
||||
15
ee/packages/federation-matrix/src/events/index.ts
Normal file
15
ee/packages/federation-matrix/src/events/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { edus } from './edu';
|
||||
import { member } from './member';
|
||||
import { message } from './message';
|
||||
import { ping } from './ping';
|
||||
import { reaction } from './reaction';
|
||||
import { room } from './room';
|
||||
|
||||
export function registerEvents() {
|
||||
void ping();
|
||||
message();
|
||||
reaction();
|
||||
member();
|
||||
void edus();
|
||||
room();
|
||||
}
|
||||
452
ee/packages/federation-matrix/src/events/member.ts
Normal file
452
ee/packages/federation-matrix/src/events/member.ts
Normal file
@@ -0,0 +1,452 @@
|
||||
import { Room, Upload } from '@rocket.chat/core-services';
|
||||
import { isBannedSubscription } from '@rocket.chat/core-typings';
|
||||
import type { IRoomNativeFederated, IRoom, IUser, RoomType } from '@rocket.chat/core-typings';
|
||||
import { federationSDK, type HomeserverEventSignatures, type PduForType } from '@rocket.chat/federation-sdk';
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { Rooms, Subscriptions, Users } from '@rocket.chat/models';
|
||||
import debounce from 'lodash.debounce';
|
||||
import mem from 'mem';
|
||||
|
||||
import { extractDomainFromMatrixUserId } from '../helpers/extractDomainFromMatrixUserId';
|
||||
import { getFederatedRoomName } from '../helpers/getFederatedRoomName';
|
||||
import { getOrCreateFederatedUser } from '../helpers/getOrCreateFederatedUser';
|
||||
import { getUsernameServername } from '../helpers/getUsernameServername';
|
||||
import { MatrixMediaService } from '../services/MatrixMediaService';
|
||||
|
||||
const logger = new Logger('federation-matrix:member');
|
||||
|
||||
async function downloadAndSetAvatar(user: IUser, avatarUrl: string | null): Promise<void> {
|
||||
try {
|
||||
// if no avatarUrl is provided, it means the user removed his avatar, so we need to set an empty avatar to remove the avatar from their side as well
|
||||
if (!avatarUrl) {
|
||||
await Upload.resetUserAvatar(user);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!avatarUrl?.startsWith('mxc://')) {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Downloading avatar for user ${user.username}: ${avatarUrl}`);
|
||||
|
||||
const parsed = MatrixMediaService.parseMXCUri(avatarUrl);
|
||||
if (!parsed) {
|
||||
logger.warn(`Invalid MXC URI: ${avatarUrl}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const buffer = await federationSDK.downloadFromRemoteServer(parsed.serverName, parsed.mediaId);
|
||||
if (!buffer) {
|
||||
logger.warn(`Failed to download avatar from ${avatarUrl}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// detect content type from buffer (basic image type detection)
|
||||
let contentType: string | undefined;
|
||||
if (buffer[0] === 0xff && buffer[1] === 0xd8) {
|
||||
contentType = 'image/jpeg';
|
||||
} else if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
|
||||
contentType = 'image/png';
|
||||
} else if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46) {
|
||||
contentType = 'image/gif';
|
||||
} else if (
|
||||
buffer[0] === 0x52 &&
|
||||
buffer[1] === 0x49 &&
|
||||
buffer[2] === 0x46 &&
|
||||
buffer[3] === 0x46 &&
|
||||
buffer[8] === 0x57 &&
|
||||
buffer[9] === 0x45 &&
|
||||
buffer[10] === 0x42 &&
|
||||
buffer[11] === 0x50
|
||||
) {
|
||||
contentType = 'image/webp';
|
||||
}
|
||||
|
||||
if (!contentType) {
|
||||
logger.warn({ msg: 'Unsupported remote avatar format from external server', username: user.username, avatarUrl });
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO need to perform a validation to check if the user actually changed avatar
|
||||
await Upload.setUserAvatar(user, buffer, contentType, 'rest');
|
||||
} catch (error) {
|
||||
logger.error({ err: error, user: user.username, msg: `Error downloading/setting avatar for user` });
|
||||
}
|
||||
}
|
||||
|
||||
async function getOrCreateFederatedRoom({
|
||||
matrixRoomId,
|
||||
roomName,
|
||||
roomFName,
|
||||
roomType,
|
||||
inviterUserId,
|
||||
inviterUsername,
|
||||
inviteeUsername,
|
||||
}: {
|
||||
matrixRoomId: string;
|
||||
roomName: string;
|
||||
roomFName: string;
|
||||
roomType: RoomType;
|
||||
inviterUserId: string;
|
||||
inviterUsername: string;
|
||||
inviteeUsername?: string;
|
||||
}): Promise<IRoom> {
|
||||
try {
|
||||
const room = await Rooms.findOne({ 'federation.mrid': matrixRoomId });
|
||||
if (room) {
|
||||
return room;
|
||||
}
|
||||
|
||||
const origin = matrixRoomId.split(':').pop();
|
||||
if (!origin) {
|
||||
throw new Error(`Room origin not found for Matrix ID: ${matrixRoomId}`);
|
||||
}
|
||||
|
||||
// TODO room creator is not always the inviter
|
||||
|
||||
return Room.create<IRoomNativeFederated>(inviterUserId, {
|
||||
type: roomType,
|
||||
name: roomName,
|
||||
members: inviteeUsername ? [inviteeUsername, inviterUsername] : [inviterUsername],
|
||||
options: {
|
||||
forceNew: true, // an invite means the room does not exist yet
|
||||
creator: inviterUserId,
|
||||
},
|
||||
extraData: {
|
||||
federated: true,
|
||||
federation: {
|
||||
version: 1,
|
||||
mrid: matrixRoomId,
|
||||
origin,
|
||||
},
|
||||
...(roomType !== 'd' && { fname: roomFName }), // DMs do not have a fname
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Error getting or creating federated room', err, roomName });
|
||||
throw new Error(`Error getting or creating federated room ${roomName}`);
|
||||
}
|
||||
}
|
||||
|
||||
// get the join rule type from the stripped state stored in the unsigned section of the event
|
||||
// as per the spec, we must support several types but we only support invite and public for now.
|
||||
// in the future, we must start looking into 'knock', 'knock_restricted', 'restricted' and 'private'.
|
||||
function getJoinRuleType(strippedState: PduForType<'m.room.join_rules'>[]): 'p' | 'c' | 'd' {
|
||||
const joinRulesState = strippedState?.find((state: PduForType<'m.room.join_rules'>) => state.type === 'm.room.join_rules');
|
||||
|
||||
// as per the spec, users need to be invited to join a room, unless the room’s join rules state otherwise.
|
||||
if (!joinRulesState) {
|
||||
return 'p';
|
||||
}
|
||||
|
||||
const joinRule = joinRulesState?.content?.join_rule;
|
||||
switch (joinRule) {
|
||||
case 'invite':
|
||||
return 'p';
|
||||
case 'public':
|
||||
return 'c';
|
||||
case 'knock':
|
||||
throw new Error(`Knock join rule is not supported`);
|
||||
case 'knock_restricted':
|
||||
throw new Error(`Knock restricted join rule is not supported`);
|
||||
case 'restricted':
|
||||
throw new Error(`Restricted join rule is not supported`);
|
||||
case 'private':
|
||||
throw new Error(`Private join rule is not supported`);
|
||||
default:
|
||||
throw new Error(`Unknown join rule type: ${joinRule}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleInvite({
|
||||
sender: senderId,
|
||||
state_key: userId,
|
||||
room_id: roomId,
|
||||
content,
|
||||
unsigned,
|
||||
}: HomeserverEventSignatures['homeserver.matrix.membership']['event']): Promise<void> {
|
||||
const inviterUser = await getOrCreateFederatedUser(senderId);
|
||||
const inviteeUser = await getOrCreateFederatedUser(userId);
|
||||
|
||||
const strippedState = unsigned.invite_room_state;
|
||||
|
||||
const joinRuleType = getJoinRuleType(strippedState);
|
||||
|
||||
const roomOriginDomain = senderId.split(':')?.pop();
|
||||
if (!roomOriginDomain) {
|
||||
throw new Error(`Room origin domain not found: ${roomId}`);
|
||||
}
|
||||
|
||||
const roomNameState = strippedState?.find((state: PduForType<'m.room.name'>) => state.type === 'm.room.name');
|
||||
const matrixRoomName = roomNameState?.content?.name;
|
||||
|
||||
// DMs do not have a join rule type (they are treated as invite only rooms),
|
||||
// so we use 'd' for direct messages translation to RC.
|
||||
const roomType = content?.is_direct || !matrixRoomName ? 'd' : joinRuleType;
|
||||
|
||||
let roomName: string;
|
||||
let roomFName: string;
|
||||
|
||||
if (roomType === 'd') {
|
||||
roomName = senderId;
|
||||
roomFName = senderId;
|
||||
} else {
|
||||
roomName = getFederatedRoomName(roomId);
|
||||
roomFName = `${matrixRoomName}:${roomOriginDomain}`;
|
||||
}
|
||||
|
||||
const room = await getOrCreateFederatedRoom({
|
||||
matrixRoomId: roomId,
|
||||
roomName,
|
||||
roomFName,
|
||||
roomType,
|
||||
inviterUserId: inviterUser._id,
|
||||
inviterUsername: inviterUser.username,
|
||||
inviteeUsername: roomType === 'd' ? inviteeUser.username : undefined,
|
||||
});
|
||||
|
||||
if (!room) {
|
||||
throw new Error(`Room not found or could not be created: ${roomId}`);
|
||||
}
|
||||
|
||||
const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, inviteeUser._id);
|
||||
if (subscription) {
|
||||
// if subscription state says the user is banned, it means the user was previously banned and is now being re-invited,
|
||||
// so we need to unban the user instead of creating a new invite
|
||||
if (isBannedSubscription(subscription)) {
|
||||
await Room.unbanAndInviteUser(subscription, inviteeUser, inviterUser);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await Room.createUserSubscription({
|
||||
ts: new Date(),
|
||||
room,
|
||||
userToBeAdded: inviteeUser,
|
||||
inviter: inviterUser,
|
||||
status: 'INVITED',
|
||||
});
|
||||
|
||||
// if an invite is sent to a DM, we need to update the room name to reflect all participants
|
||||
if (room.t === 'd') {
|
||||
await Room.updateDirectMessageRoomName(room);
|
||||
}
|
||||
}
|
||||
|
||||
const getUpdateUserNameDebounced = mem((userId: string) => debounce((name: string) => Users.setName(userId, name), 1000));
|
||||
|
||||
function updateUserNameDebounced(userId: string, newName: string): void {
|
||||
void getUpdateUserNameDebounced(userId)(newName);
|
||||
}
|
||||
|
||||
const getDownloadAndSetAvatarDebounced = mem((_userId: string) =>
|
||||
debounce((user: IUser, avatarUrl: string | null) => downloadAndSetAvatar(user, avatarUrl), 2000),
|
||||
);
|
||||
|
||||
function downloadAndSetAvatarDebounced(userId: string, user: IUser, newAvatarUrl: string | null): void {
|
||||
void getDownloadAndSetAvatarDebounced(userId)(user, newAvatarUrl);
|
||||
}
|
||||
|
||||
async function handleJoin({
|
||||
room_id: roomId,
|
||||
state_key: userId,
|
||||
content,
|
||||
}: HomeserverEventSignatures['homeserver.matrix.membership']['event']): Promise<void> {
|
||||
const joiningUser = await getOrCreateFederatedUser(userId);
|
||||
|
||||
const room = await Rooms.findOneFederatedByMrid(roomId);
|
||||
if (!room) {
|
||||
throw new Error(`Room not found while joining user ${userId} to room ${roomId}`);
|
||||
}
|
||||
|
||||
// if we receive a join event but still don't have a subscription for the user,
|
||||
// it means the join event was sent before the invite event, so we need to create the subscription and then accept the invite.
|
||||
// this will happen when for example the user is unbanned, so the leave event will remove the subscription and then we just
|
||||
// receive the join event without receiving the invite.
|
||||
let subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, joiningUser._id);
|
||||
if (!subscription) {
|
||||
const subId = await Room.createUserSubscription({
|
||||
ts: new Date(),
|
||||
room,
|
||||
userToBeAdded: joiningUser,
|
||||
});
|
||||
subscription = subId ? await Subscriptions.findOneById(subId) : null;
|
||||
}
|
||||
if (!subscription) {
|
||||
throw new Error(`Subscription not found while joining user ${userId} to room ${roomId}`);
|
||||
}
|
||||
|
||||
const senderServerName = extractDomainFromMatrixUserId(userId);
|
||||
|
||||
// handle avatar updates to membership events
|
||||
if (senderServerName !== federationSDK.getConfig('serverName')) {
|
||||
// TODO if there is no avatar_url we may want to validate first if we should remove the user avatar because if may be dealing with an old join event, and the user may have changed their avatar since then, so we need to check if the avatar_url is different from the current one before removing it
|
||||
void downloadAndSetAvatarDebounced(joiningUser._id, joiningUser, content.avatar_url || null);
|
||||
}
|
||||
|
||||
// updates user name whenever we receive a join event, because Matrix sends a new join event with the updated display name whenever a user changes their display name
|
||||
if ('displayname' in content && content.displayname !== joiningUser.name) {
|
||||
// whan a user changes the it's display name we receive a new join event for every room the user is in
|
||||
// so we need to debounce the name update to avoid updating the name multiple times in a row
|
||||
void updateUserNameDebounced(joiningUser._id, content.displayname || '');
|
||||
}
|
||||
|
||||
if (room.t === 'd') {
|
||||
await Room.updateDirectMessageRoomName(
|
||||
room,
|
||||
[subscription._id],
|
||||
[{ _id: joiningUser._id, name: content.displayname || joiningUser.name, username: joiningUser.username }],
|
||||
);
|
||||
}
|
||||
|
||||
if (!subscription.status) {
|
||||
logger.info('User is already joined to the room, skipping...');
|
||||
return;
|
||||
}
|
||||
|
||||
await Room.performAcceptRoomInvite(room, subscription, joiningUser);
|
||||
}
|
||||
|
||||
async function handleLeave({
|
||||
room_id: roomId,
|
||||
state_key: userId,
|
||||
sender,
|
||||
unsigned: { prev_content: prevContent },
|
||||
}: HomeserverEventSignatures['homeserver.matrix.membership']['event']): Promise<void> {
|
||||
const serverName = federationSDK.getConfig('serverName');
|
||||
const [username] = getUsernameServername(userId, serverName);
|
||||
|
||||
const leavingUser = await Users.findOneByUsername(username);
|
||||
if (!leavingUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [senderUsername] = getUsernameServername(sender, serverName);
|
||||
|
||||
const senderUser = await Users.findOneByUsername(senderUsername);
|
||||
if (!senderUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneFederatedByMrid(roomId);
|
||||
if (!room) {
|
||||
throw new Error(`Room not found while leaving user ${userId} from room ${roomId}`);
|
||||
}
|
||||
|
||||
// In Matrix, unban is a leave event for a previously banned user.
|
||||
// Check local subscription state to distinguish leave from unban.
|
||||
const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, leavingUser._id);
|
||||
if (subscription && isBannedSubscription(subscription)) {
|
||||
await Room.performUserUnban(room, leavingUser, senderUser);
|
||||
logger.info({ msg: 'Unbanned user via federation leave event', userId: leavingUser._id, roomId: room._id });
|
||||
return;
|
||||
}
|
||||
|
||||
// this means the leave event is actually an unban
|
||||
if (prevContent?.membership === 'ban') {
|
||||
await Room.performUserUnban(room, leavingUser, senderUser);
|
||||
} else {
|
||||
await Room.performUserRemoval(room, leavingUser);
|
||||
}
|
||||
|
||||
// update room name for DMs
|
||||
if (room.t === 'd') {
|
||||
await Room.updateDirectMessageRoomName(room);
|
||||
}
|
||||
|
||||
// TODO check if there are no pending invites to the room, and if so, delete the room
|
||||
}
|
||||
|
||||
async function handleBan({
|
||||
room_id: roomId,
|
||||
state_key: userId,
|
||||
sender: senderId,
|
||||
}: HomeserverEventSignatures['homeserver.matrix.membership']['event']): Promise<void> {
|
||||
const serverName = federationSDK.getConfig('serverName');
|
||||
const [username] = getUsernameServername(userId, serverName);
|
||||
|
||||
const bannedUser = await Users.findOneByUsername(username);
|
||||
if (!bannedUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneFederatedByMrid(roomId);
|
||||
if (!room) {
|
||||
throw new Error(`Room not found while banning user ${userId} from room ${roomId}`);
|
||||
}
|
||||
|
||||
const [senderUsername] = getUsernameServername(senderId, serverName);
|
||||
const senderUser = await Users.findOneByUsername(senderUsername);
|
||||
if (!senderUser) {
|
||||
throw new Error(`Ban sender not found locally: ${senderUsername} (Matrix id ${senderId})`);
|
||||
}
|
||||
|
||||
await Room.performUserBan(room, bannedUser, senderUser);
|
||||
}
|
||||
|
||||
async function handleMembershipRejected({
|
||||
event,
|
||||
reason,
|
||||
}: HomeserverEventSignatures['homeserver.matrix.membership.rejected']): Promise<void> {
|
||||
const room = await Rooms.findOne({ 'federation.mrid': event.room_id });
|
||||
if (!room) {
|
||||
logger.debug({ msg: 'No local room found for rejected membership event', roomId: event.room_id, reason });
|
||||
return;
|
||||
}
|
||||
|
||||
const serverName = federationSDK.getConfig('serverName');
|
||||
const [username] = getUsernameServername(event.state_key, serverName);
|
||||
|
||||
const user = await Users.findOneByUsername(username);
|
||||
if (!user) {
|
||||
logger.debug({ msg: 'User not found for rejected membership event', userId: event.state_key, reason });
|
||||
return;
|
||||
}
|
||||
|
||||
await Room.revokeInvite(room, user);
|
||||
|
||||
logger.info({
|
||||
msg: 'Revoked invite due to rejected membership event',
|
||||
userId: user._id,
|
||||
roomId: room._id,
|
||||
membership: event.content.membership,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
export function member() {
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.membership', async ({ event }) => {
|
||||
try {
|
||||
switch (event.content.membership) {
|
||||
case 'invite':
|
||||
await handleInvite(event);
|
||||
break;
|
||||
|
||||
case 'join':
|
||||
await handleJoin(event);
|
||||
break;
|
||||
|
||||
case 'leave':
|
||||
await handleLeave(event);
|
||||
break;
|
||||
|
||||
case 'ban':
|
||||
await handleBan(event);
|
||||
break;
|
||||
|
||||
default:
|
||||
logger.warn({ msg: 'Unknown membership type', membership: event.content.membership });
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Failed to process Matrix membership event', err });
|
||||
}
|
||||
});
|
||||
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.membership.rejected', async (payload) => {
|
||||
try {
|
||||
await handleMembershipRejected(payload);
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Failed to process rejected membership event', err });
|
||||
}
|
||||
});
|
||||
}
|
||||
166
ee/packages/federation-matrix/src/events/message.ts
Normal file
166
ee/packages/federation-matrix/src/events/message.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { FederationMatrix, Message } from '@rocket.chat/core-services';
|
||||
import { federationSDK } from '@rocket.chat/federation-sdk';
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { Users, Rooms, Messages } from '@rocket.chat/models';
|
||||
|
||||
import { getThreadMessageId } from '../helpers/getThreadMessageId';
|
||||
|
||||
const logger = new Logger('federation-matrix:message');
|
||||
|
||||
export function message() {
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.message', async (event) => {
|
||||
try {
|
||||
await FederationMatrix.saveFederationMessage(event);
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Error processing Matrix message', err });
|
||||
}
|
||||
});
|
||||
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.encrypted', async ({ event, event_id: eventId }) => {
|
||||
try {
|
||||
if (!event.content.ciphertext) {
|
||||
logger.debug('No message content found in event');
|
||||
return;
|
||||
}
|
||||
|
||||
// at this point we know for sure the user already exists
|
||||
const user = await Users.findOneByUsername(event.sender);
|
||||
if (!user) {
|
||||
throw new Error(`User not found for sender: ${event.sender}`);
|
||||
}
|
||||
|
||||
const room = await Rooms.findOne({ 'federation.mrid': event.room_id });
|
||||
if (!room) {
|
||||
throw new Error(`No mapped room found for room_id: ${event.room_id}`);
|
||||
}
|
||||
|
||||
const relation = event.content['m.relates_to'];
|
||||
|
||||
// SPEC: For example, an m.thread relationship type denotes that the event is part of a “thread” of messages and should be rendered as such.
|
||||
const hasRelation = relation && 'rel_type' in relation;
|
||||
|
||||
const isThreadMessage = hasRelation && relation.rel_type === 'm.thread';
|
||||
|
||||
const threadRootEventId = isThreadMessage && relation.event_id;
|
||||
|
||||
// SPEC: Though rich replies form a relationship to another event, they do not use rel_type to create this relationship.
|
||||
// Instead, a subkey named m.in_reply_to is used to describe the reply’s relationship,
|
||||
const isRichReply = relation && !('rel_type' in relation) && 'm.in_reply_to' in relation;
|
||||
|
||||
const quoteMessageEventId = isRichReply && relation['m.in_reply_to']?.event_id;
|
||||
|
||||
const thread = threadRootEventId ? await getThreadMessageId(threadRootEventId) : undefined;
|
||||
|
||||
const isEditedMessage = hasRelation && relation.rel_type === 'm.replace';
|
||||
if (isEditedMessage && relation.event_id) {
|
||||
logger.debug('Received edited message from Matrix, updating existing message');
|
||||
const originalMessage = await Messages.findOneByFederationId(relation.event_id);
|
||||
if (!originalMessage) {
|
||||
logger.error({ event_id: relation.event_id, msg: 'Original message not found for edit' });
|
||||
return;
|
||||
}
|
||||
if (originalMessage.federation?.eventId !== relation.event_id) {
|
||||
return;
|
||||
}
|
||||
if (originalMessage.content?.ciphertext === event.content.ciphertext) {
|
||||
logger.debug('No changes in message content, skipping update');
|
||||
return;
|
||||
}
|
||||
|
||||
if (quoteMessageEventId) {
|
||||
await Message.updateMessage(
|
||||
{
|
||||
...originalMessage,
|
||||
content: {
|
||||
algorithm: event.content.algorithm,
|
||||
ciphertext: event.content.ciphertext,
|
||||
},
|
||||
},
|
||||
user,
|
||||
originalMessage,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await Message.updateMessage(
|
||||
{
|
||||
...originalMessage,
|
||||
content: {
|
||||
algorithm: event.content.algorithm,
|
||||
ciphertext: event.content.ciphertext,
|
||||
},
|
||||
},
|
||||
user,
|
||||
originalMessage,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (quoteMessageEventId) {
|
||||
const originalMessage = await Messages.findOneByFederationId(quoteMessageEventId);
|
||||
if (!originalMessage) {
|
||||
logger.error({ quoteMessageEventId, msg: 'Original message not found for quote' });
|
||||
return;
|
||||
}
|
||||
await Message.saveMessageFromFederation({
|
||||
fromId: user._id,
|
||||
rid: room._id,
|
||||
e2e_content: {
|
||||
algorithm: event.content.algorithm,
|
||||
ciphertext: event.content.ciphertext,
|
||||
},
|
||||
federation_event_id: eventId,
|
||||
thread,
|
||||
ts: new Date(event.origin_server_ts),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await Message.saveMessageFromFederation({
|
||||
fromId: user._id,
|
||||
rid: room._id,
|
||||
e2e_content: {
|
||||
algorithm: event.content.algorithm,
|
||||
ciphertext: event.content.ciphertext,
|
||||
},
|
||||
federation_event_id: eventId,
|
||||
thread,
|
||||
ts: new Date(event.origin_server_ts),
|
||||
});
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Error processing Matrix message', err });
|
||||
}
|
||||
});
|
||||
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.redaction', async ({ event }) => {
|
||||
try {
|
||||
const redactedEventId = event.redacts;
|
||||
if (!redactedEventId) {
|
||||
logger.debug('No redacts field in redaction event');
|
||||
return;
|
||||
}
|
||||
|
||||
const messageEvent = await FederationMatrix.getEventById(redactedEventId);
|
||||
if (messageEvent?.event.type !== 'm.room.message') {
|
||||
logger.debug({ msg: 'Event is not a message event', eventId: redactedEventId });
|
||||
return;
|
||||
}
|
||||
|
||||
const rcMessage = await Messages.findOneByFederationId(event.redacts);
|
||||
if (!rcMessage) {
|
||||
logger.debug({ msg: 'No RC message found for event', eventId: event.redacts });
|
||||
return;
|
||||
}
|
||||
const internalUsername = event.sender;
|
||||
const user = await Users.findOneByUsername(internalUsername);
|
||||
if (!user) {
|
||||
logger.debug({ msg: 'User not found', username: internalUsername });
|
||||
return;
|
||||
}
|
||||
|
||||
await Message.deleteMessage(user, rcMessage);
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Failed to process Matrix removal redaction', err });
|
||||
}
|
||||
});
|
||||
}
|
||||
7
ee/packages/federation-matrix/src/events/ping.ts
Normal file
7
ee/packages/federation-matrix/src/events/ping.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { federationSDK } from '@rocket.chat/federation-sdk';
|
||||
|
||||
export const ping = async () => {
|
||||
federationSDK.eventEmitterService.on('homeserver.ping', async (data) => {
|
||||
console.log('Message received from homeserver', data);
|
||||
});
|
||||
};
|
||||
103
ee/packages/federation-matrix/src/events/reaction.ts
Normal file
103
ee/packages/federation-matrix/src/events/reaction.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { Message, FederationMatrix } from '@rocket.chat/core-services';
|
||||
import { federationSDK } from '@rocket.chat/federation-sdk';
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { Users, Messages } from '@rocket.chat/models'; // Rooms
|
||||
|
||||
import { unicodeToShortname } from '../utils/emojiConverter';
|
||||
|
||||
const logger = new Logger('federation-matrix:reaction');
|
||||
|
||||
export function reaction() {
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.reaction', async ({ event, event_id: eventId }) => {
|
||||
try {
|
||||
const isSetReaction = event.content?.['m.relates_to'];
|
||||
|
||||
const reactionTargetEventId = isSetReaction?.event_id;
|
||||
const reactionKey = isSetReaction?.key;
|
||||
|
||||
if (typeof reactionKey !== 'string' || reactionKey.length === 0) {
|
||||
logger.error({ reactionTargetEventId, reactionKey, msg: 'Invalid or missing reaction key in Matrix reaction event' });
|
||||
return;
|
||||
}
|
||||
|
||||
const [userPart, domain] = event.sender.split(':');
|
||||
if (!userPart || !domain) {
|
||||
logger.error({ sender: event.sender, msg: 'Invalid Matrix sender ID format' });
|
||||
return;
|
||||
}
|
||||
|
||||
const internalUsername = event.sender;
|
||||
const user = await Users.findOneByUsername(internalUsername);
|
||||
if (!user) {
|
||||
logger.error({ reactionTargetEventId, internalUsername, msg: 'No RC user mapping found for Matrix event' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isSetReaction) {
|
||||
logger.debug('No relates_to content in reaction event');
|
||||
return;
|
||||
}
|
||||
|
||||
const rcMessage = await Messages.findOneByFederationId(reactionTargetEventId);
|
||||
if (!rcMessage) {
|
||||
logger.debug({ msg: 'No RC message mapping found for Matrix event', eventId: reactionTargetEventId });
|
||||
return;
|
||||
}
|
||||
|
||||
const reactionEmoji = unicodeToShortname(reactionKey);
|
||||
await Message.reactToMessage(user._id, reactionEmoji, rcMessage._id, true);
|
||||
await Messages.setFederationReactionEventId(internalUsername, rcMessage._id, reactionEmoji, eventId);
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Failed to process Matrix reaction', err });
|
||||
}
|
||||
});
|
||||
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.redaction', async ({ event }) => {
|
||||
try {
|
||||
const redactedEventId = event.redacts;
|
||||
if (!redactedEventId) {
|
||||
logger.debug('No redacts field in redaction event');
|
||||
return;
|
||||
}
|
||||
|
||||
const reactionEvent = await FederationMatrix.getEventById(redactedEventId);
|
||||
if (!reactionEvent || reactionEvent.event.type !== 'm.reaction') {
|
||||
logger.debug({ msg: 'Event is not a reaction event', eventId: redactedEventId });
|
||||
return;
|
||||
}
|
||||
|
||||
const reactionContent = reactionEvent.event.content?.['m.relates_to'];
|
||||
if (!reactionContent) {
|
||||
logger.debug('No relates_to content in reaction event');
|
||||
return;
|
||||
}
|
||||
|
||||
const targetMessageEventId = reactionContent.event_id;
|
||||
const reactionKey = reactionContent.key;
|
||||
|
||||
if (typeof reactionKey !== 'string' || reactionKey.length === 0) {
|
||||
logger.error({ redactedEventId, reactionKey, msg: 'Invalid or missing reaction key in redacted reaction event' });
|
||||
return;
|
||||
}
|
||||
|
||||
const rcMessage = await Messages.findOneByFederationId(targetMessageEventId);
|
||||
if (!rcMessage) {
|
||||
logger.debug({ msg: 'No RC message found for event', eventId: targetMessageEventId });
|
||||
return;
|
||||
}
|
||||
|
||||
const internalUsername = event.sender;
|
||||
const user = await Users.findOneByUsername(internalUsername);
|
||||
if (!user) {
|
||||
logger.debug({ msg: 'User not found', username: internalUsername });
|
||||
return;
|
||||
}
|
||||
|
||||
const reactionEmoji = unicodeToShortname(reactionKey);
|
||||
await Message.reactToMessage(user._id, reactionEmoji, rcMessage._id, false);
|
||||
await Messages.unsetFederationReactionEventId(redactedEventId, rcMessage._id, reactionEmoji);
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Failed to process Matrix reaction redaction', err });
|
||||
}
|
||||
});
|
||||
}
|
||||
87
ee/packages/federation-matrix/src/events/room.ts
Normal file
87
ee/packages/federation-matrix/src/events/room.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Room } from '@rocket.chat/core-services';
|
||||
import { federationSDK } from '@rocket.chat/federation-sdk';
|
||||
import { Rooms, Users } from '@rocket.chat/models';
|
||||
|
||||
import { getUsernameServername } from '../helpers/getUsernameServername';
|
||||
|
||||
export function room() {
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.room.name', async ({ event }) => {
|
||||
const {
|
||||
room_id: roomId,
|
||||
content: { name },
|
||||
sender: userId,
|
||||
} = event;
|
||||
|
||||
const localRoomId = await Rooms.findOne({ 'federation.mrid': roomId }, { projection: { _id: 1 } });
|
||||
if (!localRoomId) {
|
||||
throw new Error(`mapped room not found: ${roomId}`);
|
||||
}
|
||||
|
||||
const localUserId = await Users.findOneByUsername(userId, { projection: { _id: 1 } });
|
||||
if (!localUserId) {
|
||||
throw new Error(`mapped user not found: ${userId}`);
|
||||
}
|
||||
|
||||
await Room.saveRoomName(localRoomId._id, localUserId._id, name);
|
||||
});
|
||||
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.room.topic', async ({ event }) => {
|
||||
const {
|
||||
room_id: roomId,
|
||||
content: { topic },
|
||||
sender: userId,
|
||||
} = event;
|
||||
|
||||
const localRoomId = await Rooms.findOne({ 'federation.mrid': roomId }, { projection: { _id: 1 } });
|
||||
if (!localRoomId) {
|
||||
throw new Error('mapped room not found');
|
||||
}
|
||||
|
||||
const localUser = await Users.findOneByUsername(userId, { projection: { _id: 1, federation: 1, federated: 1 } });
|
||||
if (!localUser) {
|
||||
throw new Error('mapped user not found');
|
||||
}
|
||||
|
||||
await Room.saveRoomTopic(localRoomId._id, topic, {
|
||||
_id: localUser._id,
|
||||
username: userId,
|
||||
federation: localUser.federation,
|
||||
federated: localUser.federated,
|
||||
});
|
||||
});
|
||||
|
||||
federationSDK.eventEmitterService.on('homeserver.matrix.room.role', async (data) => {
|
||||
const { room_id: roomId, user_id: userId, sender_id: senderId, role } = data;
|
||||
|
||||
const localRoomId = await Rooms.findOne({ 'federation.mrid': roomId }, { projection: { _id: 1 } });
|
||||
if (!localRoomId) {
|
||||
throw new Error('mapped room not found');
|
||||
}
|
||||
|
||||
const serverName = federationSDK.getConfig('serverName');
|
||||
|
||||
const [allegedUsernameLocal, , allegedUserLocalIsLocal] = getUsernameServername(userId, serverName);
|
||||
const localUserId = allegedUserLocalIsLocal && (await Users.findOneByUsername(allegedUsernameLocal, { projection: { _id: 1 } }));
|
||||
|
||||
if (!allegedUserLocalIsLocal) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!localUserId) {
|
||||
throw new Error('mapped user not found');
|
||||
}
|
||||
|
||||
const [senderUsername, , senderIsLocal] = getUsernameServername(senderId, serverName);
|
||||
|
||||
if (senderIsLocal) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localSenderId = await Users.findOneByUsername(senderUsername, { projection: { _id: 1 } });
|
||||
if (!localSenderId) {
|
||||
throw new Error('mapped user not found');
|
||||
}
|
||||
|
||||
await Room.addUserRoleRoomScoped(localSenderId._id, localUserId._id, localRoomId._id, role);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user