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
395 lines
13 KiB
TypeScript
395 lines
13 KiB
TypeScript
import { ServiceClassInternal, Authorization, Message, MeteorError, FederationMatrix } from '@rocket.chat/core-services';
|
|
import type { ICreateRoomParams, IRoomService } from '@rocket.chat/core-services';
|
|
import {
|
|
type AtLeast,
|
|
type IRoom,
|
|
type IUser,
|
|
type MessageTypesValues,
|
|
type ISubscription,
|
|
isOmnichannelRoom,
|
|
isRoomWithJoinCode,
|
|
} from '@rocket.chat/core-typings';
|
|
import { isUserNativeFederated } from '@rocket.chat/core-typings';
|
|
import { Rooms, Subscriptions, Users } from '@rocket.chat/models';
|
|
|
|
import { getNameForDMs } from './getNameForDMs';
|
|
import { FederationActions } from './hooks/BeforeFederationActions';
|
|
import { RoomMemberActions } from '../../../definition/IRoomTypeConfig';
|
|
import { getSubscriptionAutotranslateDefaultConfig } from '../../lib/getSubscriptionAutotranslateDefaultConfig';
|
|
import { readThread } from '../../lib/messaging/threads/functions';
|
|
import {
|
|
notifyOnSubscriptionChanged,
|
|
notifyOnSubscriptionChangedById,
|
|
notifyOnSubscriptionChangedByRoomIdAndUserId,
|
|
} from '../../lib/notifyListener';
|
|
import { readMessages } from '../../lib/readMessages';
|
|
import { performAcceptRoomInvite } from '../../lib/rooms/acceptRoomInvite';
|
|
import { addUserToRoom } from '../../lib/rooms/addUserToRoom';
|
|
import { performUserBan } from '../../lib/rooms/banUserFromRoom';
|
|
import { createRoom } from '../../lib/rooms/createRoom'; // TODO remove this import
|
|
import { executeUnbanUserFromRoom } from '../../lib/rooms/executeUnbanUserFromRoom';
|
|
import { removeUserFromRoom, performUserRemoval } from '../../lib/rooms/removeUserFromRoom';
|
|
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
|
|
import { saveRoomName } from '../../lib/rooms/settings';
|
|
import { saveRoomTopic } from '../../lib/rooms/settings/saveRoomTopic';
|
|
import { getDefaultSubscriptionPref } from '../../lib/utils/lib/getDefaultSubscriptionPref';
|
|
import { getValidRoomName } from '../../lib/utils/lib/getValidRoomName';
|
|
import { createDirectMessage } from '../../meteor-methods/messages/createDirectMessage';
|
|
import { addRoomLeader } from '../../meteor-methods/rooms/addRoomLeader';
|
|
import { addRoomModerator } from '../../meteor-methods/rooms/addRoomModerator';
|
|
import { addRoomOwner } from '../../meteor-methods/rooms/addRoomOwner';
|
|
import { removeRoomLeader } from '../../meteor-methods/rooms/removeRoomLeader';
|
|
import { removeRoomModerator } from '../../meteor-methods/rooms/removeRoomModerator';
|
|
import { removeRoomOwner } from '../../meteor-methods/rooms/removeRoomOwner';
|
|
|
|
export class RoomService extends ServiceClassInternal implements IRoomService {
|
|
protected name = 'room';
|
|
|
|
async updateDirectMessageRoomName(
|
|
room: IRoom,
|
|
ignoreStatusFromSubs?: string[],
|
|
updatedNames?: AtLeast<IUser, '_id' | 'name' | 'username'>[],
|
|
): Promise<boolean> {
|
|
if (room.t !== 'd') {
|
|
throw new Error('Invalid room type');
|
|
}
|
|
const subs = await Subscriptions.findByRoomId(room._id, { projection: { u: 1, status: 1 } }).toArray();
|
|
|
|
const uids = subs.map((sub) => sub.u._id).filter((uid) => !updatedNames?.some((user) => user._id === uid));
|
|
|
|
const roomMembers = await Users.findUsersByIds(uids, { projection: { name: 1, username: 1 } }).toArray();
|
|
|
|
const roomNames = getNameForDMs([...roomMembers, ...(updatedNames ?? [])]);
|
|
|
|
for await (const sub of subs) {
|
|
// don't update the name if the user is invited but hasn't accepted yet
|
|
if (!ignoreStatusFromSubs?.includes(sub._id) && sub.status === 'INVITED') {
|
|
continue;
|
|
}
|
|
await Subscriptions.updateOne({ _id: sub._id }, { $set: roomNames[sub.u._id] });
|
|
|
|
void notifyOnSubscriptionChangedByRoomIdAndUserId(room._id, sub.u._id, 'updated');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async create(uid: string, params: ICreateRoomParams): Promise<IRoom> {
|
|
const { type, name, members = [], readOnly, extraData, options } = params;
|
|
|
|
const hasPermission = await Authorization.hasPermission(uid, `create-${type}`);
|
|
if (!hasPermission) {
|
|
throw new Error('no-permission');
|
|
}
|
|
|
|
const user = await Users.findOneById(uid);
|
|
if (!user?.username) {
|
|
throw new Error('User not found');
|
|
}
|
|
|
|
// TODO convert `createRoom` function to "raw" and move to here
|
|
return createRoom(type, name, user, members, false, readOnly, extraData, options) as unknown as IRoom;
|
|
}
|
|
|
|
async createDirectMessage({ to, from }: { to: string; from: string }): Promise<{ rid: string }> {
|
|
const [toUser, fromUser] = await Promise.all([
|
|
Users.findOneById(to, { projection: { username: 1 } }),
|
|
Users.findOneById(from, { projection: { _id: 1 } }),
|
|
]);
|
|
|
|
if (!toUser?.username || !fromUser) {
|
|
throw new Error('error-invalid-user');
|
|
}
|
|
return this.createDirectMessageWithMultipleUsers([toUser.username], fromUser._id);
|
|
}
|
|
|
|
async createDirectMessageWithMultipleUsers(members: string[], creatorId: string): Promise<{ rid: string }> {
|
|
return createDirectMessage(members, creatorId);
|
|
}
|
|
|
|
async addMember(uid: string, rid: string): Promise<boolean> {
|
|
const hasPermission = await Authorization.hasPermission(uid, 'add-user-to-joined-room', rid);
|
|
if (!hasPermission) {
|
|
throw new Error('no-permission');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async addUserToRoom(
|
|
roomId: string,
|
|
user: Pick<IUser, '_id'>,
|
|
inviter?: Pick<IUser, '_id' | 'username'>,
|
|
options?: {
|
|
skipSystemMessage?: boolean;
|
|
skipAlertSound?: boolean;
|
|
},
|
|
): Promise<boolean | undefined> {
|
|
return addUserToRoom(roomId, user, inviter, options);
|
|
}
|
|
|
|
async removeUserFromRoom(
|
|
roomId: string,
|
|
user: IUser,
|
|
options?: { byUser?: IUser; skipAppPreEvents?: boolean; customSystemMessage?: MessageTypesValues },
|
|
): Promise<void> {
|
|
return removeUserFromRoom(roomId, user, options);
|
|
}
|
|
|
|
async performUserRemoval(room: IRoom, user: IUser, options?: { byUser?: IUser }): Promise<void> {
|
|
return performUserRemoval(room, user, options);
|
|
}
|
|
|
|
async performUserBan(room: IRoom, user: IUser, byUser: IUser): Promise<void> {
|
|
return performUserBan(room, user, byUser);
|
|
}
|
|
|
|
async performUserUnban(room: IRoom, user: IUser, byUser: IUser): Promise<void> {
|
|
return executeUnbanUserFromRoom(room._id, user, byUser);
|
|
}
|
|
|
|
async performAcceptRoomInvite(room: IRoom, subscription: ISubscription, user: IUser & { username: string }): Promise<void> {
|
|
return performAcceptRoomInvite(room, subscription, user);
|
|
}
|
|
|
|
async revokeInvite(room: IRoom, user: IUser): Promise<void> {
|
|
const subscription = await Subscriptions.removeInvitedByRoomIdAndUserId(room._id, user._id);
|
|
if (!subscription) {
|
|
return;
|
|
}
|
|
|
|
void notifyOnSubscriptionChanged(subscription, 'removed');
|
|
}
|
|
|
|
async getValidRoomName(displayName: string, roomId = '', options: { allowDuplicates?: boolean } = {}): Promise<string> {
|
|
return getValidRoomName(displayName, roomId, options);
|
|
}
|
|
|
|
async saveRoomTopic(
|
|
roomId: string,
|
|
roomTopic: string | undefined,
|
|
user: Pick<IUser, 'username' | '_id' | 'federation' | 'federated'>,
|
|
sendMessage = true,
|
|
): Promise<void> {
|
|
await saveRoomTopic(roomId, roomTopic, user, sendMessage);
|
|
}
|
|
|
|
async getRouteLink(room: AtLeast<IRoom, '_id' | 't' | 'name'>): Promise<string | boolean> {
|
|
return roomCoordinator.getRouteLink(room.t as string, { rid: room._id, name: room.name });
|
|
}
|
|
|
|
/**
|
|
* Method called by users to join a room.
|
|
*/
|
|
async join({ room, user, joinCode }: { room: IRoom; user: IUser; joinCode?: string }) {
|
|
if (!(await roomCoordinator.getRoomDirectives(room.t)?.allowMemberAction(room, RoomMemberActions.JOIN, user._id))) {
|
|
throw new MeteorError('error-not-allowed', 'Not allowed', { method: 'joinRoom' });
|
|
}
|
|
|
|
if (isOmnichannelRoom(room) && !room.open) {
|
|
throw new MeteorError('room-closed', 'Room is closed', { method: 'joinRoom' });
|
|
}
|
|
|
|
if (!(await Authorization.canAccessRoom(room, user))) {
|
|
throw new MeteorError('error-not-allowed', 'Not allowed', { method: 'joinRoom' });
|
|
}
|
|
|
|
if (
|
|
FederationActions.shouldPerformFederationAction(room) &&
|
|
!isUserNativeFederated(user) &&
|
|
!(await FederationMatrix.canUserAccessFederation(user))
|
|
) {
|
|
throw new MeteorError('error-not-authorized-federation', 'Not authorized to access federation', { method: 'joinRoom' });
|
|
}
|
|
|
|
if (isRoomWithJoinCode(room) && !(await Authorization.hasPermission(user._id, 'join-without-join-code'))) {
|
|
if (!joinCode) {
|
|
throw new MeteorError('error-code-required', 'Code required', { method: 'joinRoom' });
|
|
}
|
|
|
|
const isCorrectJoinCode = !!(await Rooms.findOneByJoinCodeAndId(joinCode, room._id, {
|
|
projection: { _id: 1 },
|
|
}));
|
|
|
|
if (!isCorrectJoinCode) {
|
|
throw new MeteorError('error-code-invalid', 'Invalid code', { method: 'joinRoom' });
|
|
}
|
|
}
|
|
|
|
return addUserToRoom(room._id, user);
|
|
}
|
|
|
|
async beforeLeave(room: IRoom): Promise<void> {
|
|
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
|
|
}
|
|
|
|
async beforeUserRemoved(room: IRoom): Promise<void> {
|
|
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
|
|
}
|
|
|
|
async beforeNameChange(room: IRoom): Promise<void> {
|
|
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
|
|
}
|
|
|
|
async beforeTopicChange(room: IRoom): Promise<void> {
|
|
FederationActions.blockIfRoomFederatedButServiceNotReady(room);
|
|
}
|
|
|
|
async saveRoomName(roomId: string, userId: string, name: string) {
|
|
const user = await Users.findOneById(userId);
|
|
if (!user) {
|
|
throw new Error('User not found');
|
|
}
|
|
await saveRoomName(roomId, name, user);
|
|
}
|
|
|
|
public async addUserRoleRoomScoped(
|
|
fromUserId: string,
|
|
userId: string,
|
|
roomId: string,
|
|
role: 'moderator' | 'owner' | 'leader' | 'user',
|
|
): Promise<void> {
|
|
if (role === 'moderator') {
|
|
await addRoomModerator(fromUserId, roomId, userId);
|
|
return;
|
|
}
|
|
|
|
if (role === 'owner') {
|
|
await addRoomOwner(fromUserId, roomId, userId);
|
|
return;
|
|
}
|
|
|
|
if (role === 'leader') {
|
|
await addRoomLeader(fromUserId, roomId, userId);
|
|
return;
|
|
}
|
|
|
|
const sub = await Subscriptions.findByUserIdAndRoomIds(userId, [roomId], { projection: { roles: 1 } }).next();
|
|
if (!sub) {
|
|
throw new Error('user and room subsciption not found');
|
|
}
|
|
|
|
if (!sub.roles) {
|
|
return; // 'user' role essentially
|
|
}
|
|
|
|
for await (const currentRole of sub.roles) {
|
|
if (currentRole === 'owner') {
|
|
await removeRoomOwner(fromUserId, roomId, userId);
|
|
return;
|
|
}
|
|
|
|
if (currentRole === 'leader') {
|
|
await removeRoomLeader(fromUserId, roomId, userId);
|
|
return;
|
|
}
|
|
|
|
if (currentRole === 'moderator') {
|
|
await removeRoomModerator(fromUserId, roomId, userId);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
async createUserSubscription({
|
|
room,
|
|
ts,
|
|
userToBeAdded,
|
|
inviter,
|
|
createAsHidden = false,
|
|
skipAlertSound = false,
|
|
skipSystemMessage = false,
|
|
status,
|
|
roles,
|
|
}: {
|
|
room: IRoom;
|
|
ts: Date;
|
|
userToBeAdded: IUser;
|
|
inviter?: Pick<IUser, '_id' | 'username' | 'name'>;
|
|
createAsHidden?: boolean;
|
|
skipAlertSound?: boolean;
|
|
skipSystemMessage?: boolean;
|
|
status?: 'INVITED';
|
|
roles?: ISubscription['roles'];
|
|
}): Promise<string | undefined> {
|
|
const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(userToBeAdded);
|
|
|
|
const { insertedId } = await Subscriptions.createWithRoomAndUser(room, userToBeAdded, {
|
|
ts,
|
|
open: !createAsHidden,
|
|
alert: createAsHidden ? false : !skipAlertSound,
|
|
unread: 1,
|
|
userMentions: 1,
|
|
groupMentions: 0,
|
|
...(roles && { roles }),
|
|
...(status && { status }),
|
|
...(inviter && { inviter: { _id: inviter._id, username: inviter.username!, ...(inviter.name && { name: inviter.name }) } }),
|
|
...autoTranslateConfig,
|
|
...getDefaultSubscriptionPref(userToBeAdded),
|
|
// `name` is optional for users, so fall back to the username like `getNameForDMs` does
|
|
...(room.t === 'd' && inviter && { fname: inviter.name || inviter.username, name: inviter.username }),
|
|
});
|
|
|
|
if (insertedId) {
|
|
void notifyOnSubscriptionChangedById(insertedId, 'inserted');
|
|
}
|
|
|
|
if (!skipSystemMessage && userToBeAdded.username) {
|
|
if (inviter) {
|
|
const extraData = {
|
|
ts,
|
|
u: {
|
|
_id: inviter._id,
|
|
username: inviter.username,
|
|
},
|
|
};
|
|
if (room.teamMain) {
|
|
await Message.saveSystemMessage('added-user-to-team', room._id, userToBeAdded.username, userToBeAdded, extraData);
|
|
} else if (status === 'INVITED') {
|
|
await Message.saveSystemMessage('ui', room._id, userToBeAdded.username, userToBeAdded, {
|
|
u: { _id: inviter._id, username: inviter.username },
|
|
});
|
|
} else {
|
|
await Message.saveSystemMessage('au', room._id, userToBeAdded.username, userToBeAdded, extraData);
|
|
}
|
|
} else if (room.prid) {
|
|
await Message.saveSystemMessage('ut', room._id, userToBeAdded.username, userToBeAdded, { ts });
|
|
} else if (room.teamMain) {
|
|
await Message.saveSystemMessage('ujt', room._id, userToBeAdded.username, userToBeAdded, { ts });
|
|
} else {
|
|
await Message.saveSystemMessage('uj', room._id, userToBeAdded.username, userToBeAdded, { ts });
|
|
}
|
|
}
|
|
|
|
return insertedId;
|
|
}
|
|
|
|
async markAsRead(room: IRoom, userId: string, readThreads = false): Promise<void> {
|
|
await readMessages(room, userId, readThreads);
|
|
}
|
|
|
|
async readThread({ user, room, tmid }: { user: IUser; room: IRoom; tmid: string }): Promise<void> {
|
|
await readThread({
|
|
user,
|
|
room,
|
|
tmid,
|
|
});
|
|
}
|
|
|
|
async unbanAndInviteUser(
|
|
subscription: ISubscription,
|
|
inviteeUser: Pick<IUser, '_id' | 'username' | 'name'>,
|
|
inviterUser: Required<Pick<IUser, '_id' | 'username'>> & Pick<IUser, 'name'>,
|
|
): Promise<void> {
|
|
await Subscriptions.unbanToInvitedById(subscription._id, inviterUser);
|
|
|
|
void notifyOnSubscriptionChangedById(subscription._id, 'updated');
|
|
|
|
if (inviteeUser?.username) {
|
|
await Message.saveSystemMessage('ui', subscription.rid, inviteeUser.username, inviteeUser, {
|
|
u: { _id: inviterUser._id, username: inviterUser.username },
|
|
});
|
|
}
|
|
}
|
|
}
|