Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
810dacf
add new routes required for federation bridges
sampaiodiego May 6, 2026
77a71fe
expose a way to save federation messages
sampaiodiego May 8, 2026
d17dc00
create room with alias
sampaiodiego May 8, 2026
ec948a3
fix bridge users not as local
sampaiodiego May 8, 2026
b37f463
support join without invite for public rooms
sampaiodiego May 8, 2026
ea68632
add xmpp slash command to join rooms
sampaiodiego May 8, 2026
d0b6cbb
fix typing
sampaiodiego May 18, 2026
de2a0a9
save user displayname
sampaiodiego May 25, 2026
33d03d7
add file upload support
sampaiodiego May 25, 2026
5b4c6ce
send files
sampaiodiego May 25, 2026
a45a87b
add profile routes for specific fields
sampaiodiego May 25, 2026
91549e6
add valid response to /client/versions
sampaiodiego May 27, 2026
01751e3
use standard logger instance for Matrix APIs
sampaiodiego May 27, 2026
e2a688f
add read receipt capability
sampaiodiego May 27, 2026
9ad5489
setup xmpp via settings
sampaiodiego Jun 1, 2026
5874ce7
remove check hack
sampaiodiego Jun 1, 2026
2a43dec
cleanup slashcommand
sampaiodiego Jun 1, 2026
c868539
chore: bump federation-sdk to version 0.7.0-beta.0
sampaiodiego Jul 3, 2026
13074e0
chore: update federation-sdk to version 0.7.0-beta.1
sampaiodiego Jul 3, 2026
8d896aa
fix usernames from xmpp
sampaiodiego Jun 16, 2026
f941172
bump federation-sdk to 0.7.0-beta.3
sampaiodiego Jun 16, 2026
1ef25d8
better error logging
sampaiodiego Jun 16, 2026
1e80d36
add error handling to xmpp slash command
sampaiodiego Jun 17, 2026
df12d02
fix profile displayname set
sampaiodiego Jun 18, 2026
e956a4f
always show the name as typing
sampaiodiego Jun 18, 2026
853bcac
change variable name to better reflect its actual value
sampaiodiego Jun 18, 2026
c01a40a
cleanup not used code
sampaiodiego Jun 25, 2026
040eddd
remove catchall route
sampaiodiego Jun 25, 2026
f6706c6
better error return
sampaiodiego Jun 25, 2026
067eaf9
add support to admin/bridge room
sampaiodiego Jun 25, 2026
921b5d7
update settings' descriptions
sampaiodiego Jun 25, 2026
96359a7
add support to ping events
sampaiodiego Jun 26, 2026
35ecb95
bump federation-sdk to 0.7.0-beta.4
sampaiodiego Jun 26, 2026
eb8d875
fix i18n
sampaiodiego Jun 26, 2026
e6431ed
rename /xmpp command to /xmpp-join
sampaiodiego Jul 1, 2026
d87d4f0
fix settings enableQuery
sampaiodiego Jul 1, 2026
22d3cd2
fix command hint
sampaiodiego Jul 1, 2026
0f80151
code cleanup
sampaiodiego Jul 1, 2026
f8a3080
add comment regarding endpoint path behavior
sampaiodiego Jul 1, 2026
7934b56
validate room and return proper error in case it is unknown
sampaiodiego Jul 1, 2026
5d9636d
dont allow user registration matching bridge namespace
sampaiodiego Jul 2, 2026
ab27ac7
fix join endpoint not returning room_id
sampaiodiego Jul 2, 2026
2d1a72f
accept optional eventType
sampaiodiego Jul 2, 2026
f04384a
accepts plain user name for client register
sampaiodiego Jul 2, 2026
2b421ca
improve empty response schema
sampaiodiego Jul 3, 2026
1d85216
improve versions response schema
sampaiodiego Jul 3, 2026
464aca0
improve presence response schema
sampaiodiego Jul 3, 2026
04bae63
declare 404 return for endpoints
sampaiodiego Jul 3, 2026
f47886a
add eventId to set topic endpoint
sampaiodiego Jul 3, 2026
0258c40
validate if both rooms and user bridge are exclusive
sampaiodiego Jul 3, 2026
8d7f28f
validate limit
sampaiodiego Jul 3, 2026
caa422f
passthrough resized thumbnail stream
sampaiodiego Jul 3, 2026
2c4bfd6
validate empty domain
sampaiodiego Jul 3, 2026
6b3eb0a
remove unused publicRooms endpoint
sampaiodiego Jul 3, 2026
08a7723
remove directory route
sampaiodiego Jul 6, 2026
dd2e0f3
fix bot being created without appservice id
sampaiodiego Jul 6, 2026
9808eed
bump federation-sdk to 0.7.0-beta.6
sampaiodiego Jul 6, 2026
bb525a2
fix review
sampaiodiego Jul 7, 2026
2b1c11c
more review fixes
sampaiodiego Jul 7, 2026
c46da7e
bump @rocket.chat/federation-sdk to 0.7.0-beta.7
sampaiodiego Jul 13, 2026
9371e87
bump federation-sdk to 0.7.0-beta.8
sampaiodiego Jul 15, 2026
8b3f1ae
register xmpp appservice
sampaiodiego Jul 15, 2026
fdf0233
bump federation-sdk to 0.7.0
sampaiodiego Jul 16, 2026
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
9 changes: 9 additions & 0 deletions apps/meteor/client/startup/slashCommands/federation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ slashCommands.add({
previewer,
previewCallback,
});

slashCommands.add({
command: 'xmpp-join',
options: {
description: 'Join xmpp rooms',
params: '#channel',
},
providesPreview: false,
});
20 changes: 19 additions & 1 deletion apps/meteor/ee/server/hooks/federation/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { FederationMatrix, Message, MeteorError, Room } from '@rocket.chat/core-services';
import { isEditedMessage, isRoomNativeFederated, isUserNativeFederated, isBannedSubscription } from '@rocket.chat/core-typings';
import type { IRoomNativeFederated, IMessage, IRoom, IUser } from '@rocket.chat/core-typings';
import { validateFederatedUsername } from '@rocket.chat/federation-matrix';
import { isReservedByExclusiveBridge, validateFederatedUsername } from '@rocket.chat/federation-matrix';
import { Rooms, Subscriptions, Users } from '@rocket.chat/models';

import { callbacks } from '../../../../server/lib/callbacks';
Expand All @@ -12,6 +12,7 @@ import { afterUnbanFromRoomCallback } from '../../../../server/lib/callbacks/aft
import { beforeAddUsersToRoom, beforeAddUserToRoom } from '../../../../server/lib/callbacks/beforeAddUserToRoom';
import { beforeChangeRoomRole } from '../../../../server/lib/callbacks/beforeChangeRoomRole';
import { prepareCreateRoomCallback } from '../../../../server/lib/callbacks/beforeCreateRoomCallback';
import { checkUsernameAvailabilityCallback } from '../../../../server/lib/callbacks/checkUsernameAvailabilityCallback';
import { notifyOnRoomChangedById, notifyOnSubscriptionChanged } from '../../../../server/lib/notifyListener';
import { FederationActions } from '../../../../server/services/room/hooks/BeforeFederationActions';

Expand Down Expand Up @@ -378,3 +379,20 @@ callbacks.add('afterSaveUser', async ({ user: userUpdated, oldUser: oldUserData
void FederationMatrix.updateUserName(userUpdated);
}
});

// Reserve handles that fall within a bridge's exclusive namespace, so a regular user cannot
// register or rename into a localpart only the bridge is allowed to own. Usernames are checked
// against the bridge's exclusive `users` namespace; room/team names against its `aliases`/`rooms`
// namespaces. Inert when federation is disabled (the helper matches against loaded appservice
// registrations, of which there are none). Every handle assignment path — user creation,
// SSO/LDAP assignment and renames, plus room renames and team creation — funnels through
// `checkUsernameAvailability`.
checkUsernameAvailabilityCallback.add(
Comment thread
sampaiodiego marked this conversation as resolved.
(name, type) => {
if (isReservedByExclusiveBridge(type, name)) {
throw new MeteorError('error-username-reserved-by-bridge', 'Name is reserved by a federation bridge');
}
},
callbacks.priority.HIGH,
'federation-bridge-namespace',
);
67 changes: 61 additions & 6 deletions apps/meteor/ee/server/startup/federation.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { api, FederationMatrix as FederationMatrixService } from '@rocket.chat/core-services';
import type { SlashCommandCallbackParams } from '@rocket.chat/core-typings';
import { FederationMatrix, configureFederationMatrixSettings, setupFederationMatrix } from '@rocket.chat/federation-matrix';
import { InstanceStatus } from '@rocket.chat/instance-status';
import { License } from '@rocket.chat/license';
import { Logger } from '@rocket.chat/logger';
import { Users } from '@rocket.chat/models';

import { i18n } from '../../../server/lib/i18n';
import { slashCommands } from '../../../server/lib/utils/slashCommand';
import { StreamerCentral } from '../../../server/modules/streamer/streamer.module';
import { settings } from '../../../server/settings';
import { registerFederationRoutes } from '../api/federation';
Expand All @@ -20,7 +24,7 @@ const configureFederation = async () => {
}

try {
configureFederationMatrixSettings({
await configureFederationMatrixSettings({
instanceId: InstanceStatus.id(),
domain: settings.get('Federation_Service_Domain'),
signingKey: settings.get('Federation_Service_Matrix_Signing_Key'),
Expand All @@ -31,6 +35,10 @@ const configureFederation = async () => {
processEDUTyping: settings.get('Federation_Service_EDU_Process_Typing'),
processEDUPresence: settings.get('Federation_Service_EDU_Process_Presence'),
processEDUReceipt: settings.get('Federation_Service_EDU_Process_Receipt'),
xmppEnabled: settings.get('Federation_XMPP_Enabled'),
xmppBridgeURL: settings.get('Federation_XMPP_Bridge_URL'),
xmppBridgeHSToken: settings.get('Federation_XMPP_Bridge_HS_Token'),
xmppBridgeASToken: settings.get('Federation_XMPP_Bridge_AS_Token'),
});
} catch (err) {
logger.error({ msg: 'Failed to start federation-matrix service', err });
Expand All @@ -55,6 +63,16 @@ export const startFederationService = async (): Promise<void> => {
}
});

// `setupFederationMatrix()` runs the SDK's `init()`, which registers the DB
// collections (including `AppServiceStateCollection`). It must complete
// before the settings watcher below, whose initial fire calls `setConfig`
// and resolves repositories that depend on those collections.
try {
await setupFederationMatrix();
} catch (err) {
logger.error({ msg: 'Failed to setup federation-matrix:', err });
}

settings.watchMultiple(
[
'Federation_Service_Enabled',
Expand All @@ -67,15 +85,52 @@ export const startFederationService = async (): Promise<void> => {
'Federation_Service_Matrix_Signing_Version',
'Federation_Service_Join_Encrypted_Rooms',
'Federation_Service_Join_Non_Private_Rooms',
'Federation_XMPP_Enabled',
'Federation_XMPP_Bridge_URL',
'Federation_XMPP_Bridge_HS_Token',
'Federation_XMPP_Bridge_AS_Token',
],
async () => {
await configureFederation();
},
);

try {
await setupFederationMatrix();
} catch (err) {
logger.error({ msg: 'Failed to setup federation-matrix:', err });
}
slashCommands.add({
command: 'xmpp-join',
callback: async ({ params, message, userId }: SlashCommandCallbackParams<'xmpp-join'>): Promise<void> => {
// the helper advertises `#channel`, so accept the leading # and strip it before joining
const channel = params.trim().replace(/^#/, '');
if (!channel) {
void api.broadcast('notify.ephemeralMessage', userId, message.rid, {
msg: i18n.t('Federation_XMPP_Join_Channel_Required', {
lng: settings.get('Language') || 'en',
}),
});
return;
}

const user = await Users.findOneById(userId);
if (!user) {
logger.error({ msg: 'User not found for joining xmpp room', userId });
return;
}

const joined = await FederationMatrixService.joinAppServiceRoom(`_xmpp_${channel}`, user);

const lng = settings.get<string>('Language') || 'en';
if (joined) {
void api.broadcast('notify.ephemeralMessage', userId, message.rid, {
msg: `${i18n.t('Federation_XMPP_Join_Channel_Success', { lng })}`,
});
} else {
void api.broadcast('notify.ephemeralMessage', userId, message.rid, {
msg: `${i18n.t('Federation_XMPP_Join_Channel_Failed', { lng })}`,
});
}
},
options: {
description: 'Join xmpp rooms',
params: '#channel',
},
});
};
2 changes: 1 addition & 1 deletion apps/meteor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/favicon": "workspace:^",
"@rocket.chat/federation-matrix": "workspace:^",
"@rocket.chat/federation-sdk": "0.6.3",
"@rocket.chat/federation-sdk": "0.7.0",
"@rocket.chat/fuselage": "^0.82.0",
"@rocket.chat/fuselage-forms": "~1.5.0",
"@rocket.chat/fuselage-hooks": "~0.43.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Callbacks } from './callbacksBase';

/**
* Discriminates what kind of handle is being validated, so handlers can check the right
* namespace: `user` for a person's username, `room` for a room/team name. Rooms, teams
* and users share one local handle namespace.
*/
export type UsernameAvailabilityCheckType = 'user' | 'room';

/**
* Runs while validating whether a handle may be used (user creation, SSO/LDAP assignment and
* renames, plus room renames and team creation, all funnel through `checkUsernameAvailability`).
* Handlers should throw to reject the handle. The `type` tells the handler whether a user or
* a room/team is being validated. With no handler registered this is a no-op.
*/
export const checkUsernameAvailabilityCallback =
Callbacks.create<(username: string, type: UsernameAvailabilityCheckType) => void>('checkUsernameAvailability');
12 changes: 7 additions & 5 deletions apps/meteor/server/lib/media/file-upload/lib/FileUpload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,20 +140,22 @@ export const FileUpload = {
},

async validateFileUpload(file: IUpload, content?: Buffer | string) {
if (!Match.test(file.rid, String)) {
const isFederationUpload = Boolean(file.federation?.mxcUri);

if (!isFederationUpload && !Match.test(file.rid, String)) {
return false;
}

// livechat users can upload files but they don't have an userId
const user = (file.userId && (await Users.findOne(file.userId))) || undefined;

const room = await Rooms.findOneById(file.rid);
if (!room) {
const room = file.rid ? await Rooms.findOneById(file.rid) : undefined;
if (!isFederationUpload && !room) {
return false;
}
const directMessageAllowed = settings.get('FileUpload_Enabled_Direct');
const fileUploadAllowed = settings.get('FileUpload_Enabled');
if (user?.type !== 'app' && (await canAccessRoomAsync(room, user, file)) !== true) {
if (!isFederationUpload && room && user?.type !== 'app' && (await canAccessRoomAsync(room, user, file)) !== true) {
return false;
}
const language = user?.language || 'en';
Expand All @@ -162,7 +164,7 @@ export const FileUpload = {
throw new Meteor.Error('error-file-upload-disabled', reason);
}

if (!directMessageAllowed && room.t === 'd') {
if (room && !directMessageAllowed && room.t === 'd') {
const reason = i18n.t('File_not_allowed_direct_messages', { lng: language });
throw new Meteor.Error('error-direct-message-file-upload-not-allowed', reason);
}
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/server/lib/rooms/settings/saveRoomName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ const updateFName = async (rid: string, displayName: string): Promise<(UpdateRes
};

const updateRoomName = async (rid: string, displayName: string, slugifiedRoomName: string) => {
// Check if the username is available
if (!(await checkUsernameAvailability(slugifiedRoomName))) {
// Check if the name is available
if (!(await checkUsernameAvailability(slugifiedRoomName, 'room'))) {
throw new Meteor.Error('error-duplicate-handle', `A room, team or user with name '${slugifiedRoomName}' already exists`, {
function: 'RocketChat.updateRoomName',
handle: slugifiedRoomName,
Expand Down
12 changes: 10 additions & 2 deletions apps/meteor/server/lib/users/checkUsernameAvailability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Meteor } from 'meteor/meteor';
import _ from 'underscore';

import { settings } from '../../settings';
import type { UsernameAvailabilityCheckType } from '../callbacks/checkUsernameAvailabilityCallback';
import { checkUsernameAvailabilityCallback } from '../callbacks/checkUsernameAvailabilityCallback';
import { validateName } from '../shared/validateName';

let usernameBlackList: RegExp[] = [];
Expand Down Expand Up @@ -39,7 +41,7 @@ export const checkUsernameAvailabilityWithValidation = async function (userId: s
return checkUsernameAvailability(username);
};

export const checkUsernameAvailability = async function (username: string): Promise<boolean> {
export const checkUsernameAvailability = async function (username: string, type: UsernameAvailabilityCheckType = 'user'): Promise<boolean> {
if (usernameIsBlocked(username, usernameBlackList) || !validateName(username)) {
throw new Meteor.Error('error-blocked-username', `${_.escape(username)} is blocked and can't be used!`, {
method: 'checkUsernameAvailability',
Expand All @@ -61,5 +63,11 @@ export const checkUsernameAvailability = async function (username: string): Prom
return false;
}

return true;
try {
await checkUsernameAvailabilityCallback.run(username, type);

return true;
} catch (error) {
return false;
}
Comment thread
sampaiodiego marked this conversation as resolved.
};
2 changes: 1 addition & 1 deletion apps/meteor/server/services/team/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService {
protected name = 'team';

async create(uid: string, { team, room = { name: team.name, extraData: {} }, members, owner }: ITeamCreateParams): Promise<ITeam> {
if (!(await checkUsernameAvailability(team.name))) {
if (!(await checkUsernameAvailability(team.name, 'room'))) {
throw new Error('team-name-already-exists');
}

Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/server/services/upload/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ const logger = new Logger('UploadService');
export class UploadService extends ServiceClassInternal implements IUploadService {
protected name = 'upload';

async uploadFile({ buffer, details }: IUploadFileParams): Promise<IUpload> {
async uploadFile({ buffer, details, federation }: IUploadFileParams): Promise<IUpload> {
const fileStore = FileUpload.getStore('Uploads');
return fileStore.insert(details, buffer);
return fileStore.insert({ ...details, ...(federation && { federation }) }, buffer);
}

async sendFileMessage({ roomId, file, userId, message }: ISendFileMessageParams): Promise<boolean | undefined> {
Expand Down
35 changes: 35 additions & 0 deletions apps/meteor/server/settings/federation-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,5 +116,40 @@ export const createFederationServiceSettings = async (): Promise<void> => {
modules: ['federation'],
invalidValue: false,
});

await this.section('XMPP', async function () {
await this.add('Federation_XMPP_Enabled', false, {
type: 'boolean',
enterprise: true,
modules: ['federation'],
i18nLabel: 'Enabled',
invalidValue: false,
enableQuery: { _id: 'Federation_Service_Enabled', value: true },
});

await this.add('Federation_XMPP_Bridge_URL', '', {
type: 'string',
enterprise: true,
modules: ['federation'],
invalidValue: '',
enableQuery: { _id: 'Federation_XMPP_Enabled', value: true },
});

await this.add('Federation_XMPP_Bridge_HS_Token', '', {
type: 'password',
enterprise: true,
modules: ['federation'],
invalidValue: '',
enableQuery: { _id: 'Federation_XMPP_Enabled', value: true },
});

await this.add('Federation_XMPP_Bridge_AS_Token', '', {
type: 'password',
enterprise: true,
modules: ['federation'],
invalidValue: '',
enableQuery: { _id: 'Federation_XMPP_Enabled', value: true },
});
});
});
};
2 changes: 1 addition & 1 deletion ee/packages/federation-matrix/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@rocket.chat/core-services": "workspace:^",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/emitter": "^0.33.0",
"@rocket.chat/federation-sdk": "0.6.3",
"@rocket.chat/federation-sdk": "0.7.0",
"@rocket.chat/http-router": "workspace:^",
"@rocket.chat/license": "workspace:^",
"@rocket.chat/models": "workspace:^",
Expand Down
Loading
Loading