Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,6 @@ export async function saveRoomName(
await Message.saveSystemMessage('r', rid, displayName, user);
}

await callbacks.run('afterRoomNameChange', { room, name: displayName, oldName: room.name, userId: user._id });
await callbacks.run('afterRoomNameChange', { room, name: displayName, oldName: room.name, user });
return displayName;
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ export const saveRoomTopic = async (
if (update && sendMessage) {
await Message.saveSystemMessage('room_changed_topic', rid, roomTopic || '', user);
}
await callbacks.run('afterRoomTopicChange', undefined, { room, topic: roomTopic, userId: user._id });
await callbacks.run('afterRoomTopicChange', undefined, { room, topic: roomTopic, user });
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Pass userId for compatibility with existing handlers.

Current payload changed to user. Provide userId too to minimize breakage and align with callbacks type update.

Apply this diff:

-	await callbacks.run('afterRoomTopicChange', undefined, { room, topic: roomTopic, user });
+	await callbacks.run('afterRoomTopicChange', undefined, { room, topic: roomTopic, user, userId: user._id });

Additionally, consider typing the parameter as AtLeast<IUser, '_id' | 'username'> or loading the full IUser before invoking callbacks.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
await callbacks.run('afterRoomTopicChange', undefined, { room, topic: roomTopic, user });
await callbacks.run('afterRoomTopicChange', undefined, { room, topic: roomTopic, user, userId: user._id });
🤖 Prompt for AI Agents
In apps/meteor/app/channel-settings/server/functions/saveRoomTopic.ts around
line 31, the callbacks.run('afterRoomTopicChange', ...) call currently passes
user but omits userId; update the payload to include userId (user._id) alongside
user to maintain compatibility with existing handlers, and consider typing the
parameter as AtLeast<IUser, '_id' | 'username'> or load/ensure a full IUser
before invoking callbacks so the userId and username are present.

return update;
};
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ callbacks.add(

callbacks.add(
'afterDeleteMessage',
async (message, { _id, prid }) => {
async (message, { room: { _id, prid } }) => {
if (prid) {
const room = await Rooms.findOneById(_id, {
projection: {
Expand Down
7 changes: 6 additions & 1 deletion apps/meteor/app/lib/server/functions/createDirectRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export async function createDirectRoom(
options: {
creator?: string;
subscriptionExtra?: ISubscriptionExtraData;
federatedRoomId?: string;
},
): Promise<ICreatedRoom> {
const maxUsers = settings.get<number>('DirectMesssage_maxUsers') || 1;
Expand Down Expand Up @@ -179,7 +180,11 @@ export async function createDirectRoom(
if (isNewRoom) {
const insertedRoom = await Rooms.findOneById(rid);

await callbacks.run('afterCreateDirectRoom', insertedRoom, { members: roomMembers, creatorId: options?.creator });
await callbacks.run('afterCreateDirectRoom', insertedRoom, {
members: roomMembers,
creatorId: options?.creator,
mrid: options?.federatedRoomId,
});

void Apps.self?.triggerEvent(AppEvents.IPostRoomCreate, insertedRoom);
}
Expand Down
19 changes: 12 additions & 7 deletions apps/meteor/app/lib/server/functions/createRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,18 @@ export const createRoom = async <T extends RoomType>(
rid: string;
}
> => {
const { teamId, ...extraData } = roomExtraData || ({} as IRoom);
const { teamId, ...optionalExtraData } = roomExtraData || ({} as IRoom);

const extraData = {
...optionalExtraData,
...(optionalExtraData.federated && {
federated: true,
federation: {
version: 1,
// TODO we should be able to provide all values from here, currently we update on callback afterCreateRoom
},
}),
};

await prepareCreateRoomCallback.run({
type,
Expand Down Expand Up @@ -190,12 +201,6 @@ export const createRoom = async <T extends RoomType>(
fname: name,
_updatedAt: now,
...extraData,
...(extraData.federated && {
federated: true,
federation: {
version: 1,
},
}),
name: isDiscussion ? name : await getValidRoomName(name.trim(), undefined),
t: type,
msgs: 0,
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/lib/server/functions/deleteMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export async function deleteMessage(message: IMessage, user: IUser): Promise<voi
await Rooms.decreaseMessageCountById(message.rid, 1);
}

await callbacks.run('afterDeleteMessage', deletedMsg, room);
await callbacks.run('afterDeleteMessage', deletedMsg, { room, user });

void notifyOnRoomChangedById(message.rid);

Expand Down
19 changes: 1 addition & 18 deletions apps/meteor/app/lib/server/functions/setUserActiveStatus.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Federation, FederationEE, License } from '@rocket.chat/core-services';
import type { IUser, IUserEmail } from '@rocket.chat/core-typings';
import { isUserFederated, isDirectMessageRoom } from '@rocket.chat/core-typings';
import { Rooms, Users, Subscriptions, MatrixBridgedUser } from '@rocket.chat/models';
import { Rooms, Users, Subscriptions } from '@rocket.chat/models';
import { Accounts } from 'meteor/accounts-base';
import { check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -71,22 +70,6 @@ export async function setUserActiveStatus(
});
}

if (user.active !== active) {
const remoteUser = await MatrixBridgedUser.getExternalUserIdByLocalUserId(userId);

if (remoteUser) {
if (active) {
throw new Meteor.Error('error-not-allowed', 'Deactivated federated users can not be re-activated', {
method: 'setUserActiveStatus',
});
}

const federation = (await License.hasValidLicense()) ? FederationEE : Federation;

await federation.deactivateRemoteUser(remoteUser);
}
}

// Users without username can't do anything, so there is no need to check for owned rooms
if (user.username != null && !active) {
const userAdmin = await Users.findOneAdmin(userId || '');
Expand Down
8 changes: 1 addition & 7 deletions apps/meteor/app/lib/server/methods/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,7 @@ export async function executeSendMessage(uid: IUser['_id'], message: AtLeast<IMe
}
}

const user = await Users.findOneById(uid, {
projection: {
username: 1,
type: 1,
name: 1,
},
});
const user = await Users.findOneById(uid);
if (!user?.username) {
throw new Meteor.Error('error-invalid-user', 'Invalid user');
}
Expand Down
78 changes: 44 additions & 34 deletions apps/meteor/ee/server/hooks/federation/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FederationMatrix } from '@rocket.chat/core-services';
import { isEditedMessage, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings';
import { MatrixBridgedRoom, Rooms } from '@rocket.chat/models';
import { isEditedMessage, isUserNativeFederated, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings';
import { Rooms } from '@rocket.chat/models';

import { callbacks } from '../../../../lib/callbacks';
import { afterLeaveRoomCallback } from '../../../../lib/callbacks/afterLeaveRoomCallback';
Expand All @@ -18,56 +18,60 @@ callbacks.add('federation.afterCreateFederatedRoom', async (room, { owner, origi

if (!federatedRoomId) {
// if room if exists, we don't want to create it again
// adds bridge record
await FederationMatrix.createRoom(room, owner, members);
} else {
// matrix room was already created and passed
const fromServer = federatedRoomId.split(':')[1];
await MatrixBridgedRoom.createOrUpdateByLocalRoomId(room._id, federatedRoomId, fromServer);
}

await Rooms.setAsFederated(room._id);
await Rooms.setAsFederated(room._id, {
mrid: federatedRoomId,
origin: fromServer,
});
}
}
});

callbacks.add(
'afterSaveMessage',
async (message, { room, user }) => {
if (FederationActions.shouldPerformFederationAction(room)) {
const shouldBeHandledByFederation = room.federated === true || user.username?.includes(':');

if (shouldBeHandledByFederation) {
try {
// TODO: Check if message already exists in the database, if it does, don't send it to the federation to avoid loops
// If message is federated, it will save external_message_id like into the message object
// if this prop exists here it should not be sent to the federation to avoid loops
if (!message.federation?.eventId) {
await FederationMatrix.sendMessage(message, room, user);
}
} catch (error) {
// Log the error but don't prevent the message from being sent locally
console.error('[sendMessage] Failed to send message to Native Federation:', error);
}
if (!FederationActions.shouldPerformFederationAction(room)) {
return;
}

try {
// TODO: Check if message already exists in the database, if it does, don't send it to the federation to avoid loops
// If message is federated, it will save external_message_id like into the message object
// if this prop exists here it should not be sent to the federation to avoid loops
if (!message.federation?.eventId) {
await FederationMatrix.sendMessage(message, room, user);
}
} catch (error) {
// Log the error but don't prevent the message from being sent locally
console.error('[sendMessage] Failed to send message to Native Federation:', error);
}
},
callbacks.priority.HIGH,
'federation-v2-after-room-message-sent',
);

callbacks.add(
'afterDeleteMessage',
async (message: IMessage, room) => {
async (message: IMessage, { room, user }) => {
if (!message.federation?.eventId) {
return;
}

const isFromExternalUser = message.u?.username?.includes(':');
if (isFromExternalUser) {
// removing messages from external users is not allowed
// TODO should we make it work for external users?
if (user.federated) {
return;
}

if (!isUserNativeFederated(user)) {
return;
}
if (FederationActions.shouldPerformFederationAction(room)) {
await FederationMatrix.deleteMessage(message);
await FederationMatrix.deleteMessage(room.federation.mrid, message, user.federation.mui);
}
},
callbacks.priority.MEDIUM,
Expand Down Expand Up @@ -146,7 +150,7 @@ afterLeaveRoomCallback.add(
afterRemoveFromRoomCallback.add(
async (data: { removedUser: IUser; userWhoRemoved: IUser }, room: IRoom): Promise<void> => {
if (FederationActions.shouldPerformFederationAction(room)) {
await FederationMatrix.kickUser(room._id, data.removedUser, data.userWhoRemoved);
await FederationMatrix.kickUser(room, data.removedUser, data.userWhoRemoved);
}
},
callbacks.priority.HIGH,
Expand All @@ -155,9 +159,9 @@ afterRemoveFromRoomCallback.add(

callbacks.add(
'afterRoomNameChange',
async ({ room, name, userId }) => {
async ({ room, name, user }) => {
if (FederationActions.shouldPerformFederationAction(room)) {
await FederationMatrix.updateRoomName(room._id, name, userId);
await FederationMatrix.updateRoomName(room._id, name, user);
}
},
callbacks.priority.HIGH,
Expand All @@ -166,9 +170,9 @@ callbacks.add(

callbacks.add(
'afterRoomTopicChange',
async (_, { room, topic, userId }) => {
async (_, { room, topic, user }) => {
if (FederationActions.shouldPerformFederationAction(room)) {
await FederationMatrix.updateRoomTopic(room._id, topic, userId);
await FederationMatrix.updateRoomTopic(room, topic, user);
}
},
callbacks.priority.HIGH,
Expand All @@ -182,9 +186,8 @@ callbacks.add(
if (!isEditedMessage(message)) {
return;
}
FederationActions.shouldPerformFederationAction(room);

await FederationMatrix.updateMessage(message._id, message.msg, message.u);
await FederationMatrix.updateMessage(room, message);
}
},
callbacks.priority.HIGH,
Expand All @@ -194,7 +197,7 @@ callbacks.add(
beforeChangeRoomRole.add(
async (params: { fromUserId: string; userId: string; room: IRoom; role: 'moderator' | 'owner' | 'leader' | 'user' }) => {
if (FederationActions.shouldPerformFederationAction(params.room)) {
await FederationMatrix.addUserRoleRoomScoped(params.room._id, params.fromUserId, params.userId, params.role);
await FederationMatrix.addUserRoleRoomScoped(params.room, params.fromUserId, params.userId, params.role);
}
},
callbacks.priority.HIGH,
Expand All @@ -214,7 +217,14 @@ callbacks.add(

callbacks.add(
'afterCreateDirectRoom',
async (room: IRoom, params: { members: IUser[]; creatorId: IUser['_id'] }): Promise<void> => {
async (room: IRoom, params: { members: IUser[]; creatorId: IUser['_id']; mrid?: string }): Promise<void> => {
if (params.mrid) {
await Rooms.setAsFederated(room._id, {
mrid: params.mrid,
origin: params.mrid.split(':').pop()!,
});
return;
}
if (FederationActions.shouldPerformFederationAction(room)) {
await FederationMatrix.createDirectMessageRoom(room, params.members, params.creatorId);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/ee/server/lib/engagementDashboard/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const handleMessagesSent = async (message: IMessage, { room }: { room?: I
return message;
};

export const handleMessagesDeleted = async (message: IMessage, room?: IRoom): Promise<IMessage> => {
export const handleMessagesDeleted = async (message: IMessage, { room }: { room: IRoom }): Promise<IMessage> => {
const roomTypesToShow = roomCoordinator.getTypesToShowOnDashboard();
if (!room || !roomTypesToShow.includes(room.t)) {
return message;
Expand Down
8 changes: 4 additions & 4 deletions apps/meteor/lib/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ interface EventLikeCallbackSignatures {
'afterCreateChannel': (owner: IUser, room: IRoom) => void;
'afterCreatePrivateGroup': (owner: IUser, room: IRoom) => void;
'afterDeactivateUser': (user: IUser) => void;
'afterDeleteMessage': (message: IMessage, room: IRoom) => void;
'afterDeleteMessage': (message: IMessage, params: { room: IRoom; user: IUser }) => void;
'workspaceLicenseChanged': (license: string) => void;
'workspaceLicenseRemoved': () => void;
'afterReadMessages': (rid: IRoom['_id'], params: { uid: IUser['_id']; lastSeen?: Date; tmid?: IMessage['_id'] }) => void;
'beforeReadMessages': (rid: IRoom['_id'], uid: IUser['_id']) => void;
'afterDeleteUser': (user: IUser) => void;
'afterFileUpload': (params: { user: IUser; room: IRoom; message: IMessage }) => void;
'afterRoomNameChange': (params: { room: IRoom; name: string; oldName: string; userId: IUser['_id'] }) => void;
'afterRoomNameChange': (params: { room: IRoom; name: string; oldName: string; user: IUser }) => void;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid breaking existing listeners: keep userId as a backward‑compat payload field.

Many callbacks likely read params.userId. Add an optional userId while introducing user to avoid ecosystem breakage.

Apply this diff:

-	'afterRoomNameChange': (params: { room: IRoom; name: string; oldName: string; user: IUser }) => void;
+	'afterRoomNameChange': (params: { room: IRoom; name: string; oldName: string; user: IUser; userId?: IUser['_id'] }) => void;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'afterRoomNameChange': (params: { room: IRoom; name: string; oldName: string; user: IUser }) => void;
'afterRoomNameChange': (params: { room: IRoom; name: string; oldName: string; user: IUser; userId?: IUser['_id'] }) => void;
🤖 Prompt for AI Agents
In apps/meteor/lib/callbacks.ts around line 51, the afterRoomNameChange callback
type was changed to include user but removed the legacy userId; update the
params type to include both the new user?: IUser and the backward-compatible
userId?: string (optional) so existing listeners that read params.userId
continue to work while new code can use params.user.

'afterSaveMessage': (message: IMessage, params: { room: IRoom; user: IUser; roomUpdater?: Updater<IRoom> }) => void;
'afterOmnichannelSaveMessage': (message: IMessage, constant: { room: IOmnichannelRoom; roomUpdater: Updater<IOmnichannelRoom> }) => void;
'livechat.removeAgentDepartment': (params: { departmentId: ILivechatDepartmentRecord['_id']; agentsId: ILivechatAgent['_id'][] }) => void;
Expand All @@ -64,7 +64,7 @@ interface EventLikeCallbackSignatures {
user: AtLeast<IUser, '_id' | 'federated' | 'roles'>;
inviter: AtLeast<IUser, '_id' | 'username'>;
}) => void;
'afterCreateDirectRoom': (params: IRoom, second: { members: IUser[]; creatorId: IUser['_id'] }) => void;
'afterCreateDirectRoom': (params: IRoom, second: { members: IUser[]; creatorId: IUser['_id']; mrid?: string }) => void;
'beforeDeleteRoom': (params: IRoom) => void;
'beforeJoinDefaultChannels': (user: IUser) => void;
'beforeCreateChannel': (owner: IUser, room: IRoom) => void;
Expand Down Expand Up @@ -205,7 +205,7 @@ type ChainedCallbackSignatures = {
'roomAvatarChanged': (room: IRoom) => void;
'beforeGetMentions': (mentionIds: string[], teamMentions: MessageMention[]) => Promise<string[]>;
'livechat.manageDepartmentUnit': (params: { userId: string; departmentId: string; unitId?: string }) => void;
'afterRoomTopicChange': (params: undefined, { room, topic, userId }: { room: IRoom; topic: string; userId: IUser['_id'] }) => void;
'afterRoomTopicChange': (params: undefined, { room, topic, user }: { room: IRoom; topic: string; user: IUser }) => void;
};
Comment on lines +208 to 209
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Match topic-change callback payload with name-change: add optional userId.

Keeps parity and eases migration for handlers still expecting userId.

Apply this diff:

-	'afterRoomTopicChange': (params: undefined, { room, topic, user }: { room: IRoom; topic: string; user: IUser }) => void;
+	'afterRoomTopicChange': (params: undefined, { room, topic, user, userId }: { room: IRoom; topic: string; user: IUser; userId?: IUser['_id'] }) => void;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
'afterRoomTopicChange': (params: undefined, { room, topic, user }: { room: IRoom; topic: string; user: IUser }) => void;
};
'afterRoomTopicChange': (params: undefined, { room, topic, user, userId }: { room: IRoom; topic: string; user: IUser; userId?: IUser['_id'] }) => void;
};
🤖 Prompt for AI Agents
In apps/meteor/lib/callbacks.ts around lines 208-209, the afterRoomTopicChange
callback payload currently types the second parameter as { room: IRoom; topic:
string; user: IUser }; update that type to include an optional userId property
to match name-change parity — e.g. { room: IRoom; topic: string; user: IUser;
userId?: string } — so handlers that still expect userId continue to work;
adjust any related type exports or usages if needed.


export type Hook =
Expand Down
4 changes: 0 additions & 4 deletions apps/meteor/server/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ import {
LivechatTriggerRaw,
LivechatVisitorsRaw,
LoginServiceConfigurationRaw,
MatrixBridgedRoomRaw,
MatrixBridgedUserRaw,
MessageReadsRaw,
MessagesRaw,
MigrationsRaw,
Expand Down Expand Up @@ -132,8 +130,6 @@ registerModel('ILivechatPriorityModel', new LivechatPriorityRaw(db));
registerModel('ILivechatTriggerModel', new LivechatTriggerRaw(db));
registerModel('ILivechatVisitorsModel', new LivechatVisitorsRaw(db));
registerModel('ILoginServiceConfigurationModel', new LoginServiceConfigurationRaw(db));
registerModel('IMatrixBridgedRoomModel', new MatrixBridgedRoomRaw(db));
registerModel('IMatrixBridgedUserModel', new MatrixBridgedUserRaw(db));
registerModel('IMessageReadsModel', new MessageReadsRaw(db));
registerModel('IMessagesModel', new MessagesRaw(db, trashCollection));
registerModel('IMigrationsModel', new MigrationsRaw(db));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { IMatrixFederationStatistics } from '@rocket.chat/core-typings';
import { MatrixBridgedRoom, Rooms, Users } from '@rocket.chat/models';
import { Rooms, Users } from '@rocket.chat/models';

import { settings } from '../../../../../../app/settings/server';

Expand Down Expand Up @@ -45,9 +45,7 @@ class RocketChatStatisticsAdapter {
}

async getAmountOfConnectedExternalServers(): Promise<{ quantity: number; servers: string[] }> {
const externalServers = await MatrixBridgedRoom.getExternalServerConnectedExcluding(
settings.get('Federation_Matrix_homeserver_domain'),
);
const externalServers = await Rooms.countDistinctFederationRoomsExcluding(settings.get('Federation_Matrix_homeserver_domain'));

return {
quantity: externalServers.length,
Expand Down
Loading
Loading