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:
40
apps/meteor/server/meteor-methods/auth/addOAuthService.ts
Normal file
40
apps/meteor/server/meteor-methods/auth/addOAuthService.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { addOAuthService } from '../../lib/oauth/addOAuthService';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
addOAuthService(name: string): void;
|
||||
}
|
||||
}
|
||||
|
||||
export const addOAuthServiceMethod = async (userId: string, name: string): Promise<void> => {
|
||||
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Adding OAuth Services is not allowed', {
|
||||
method: 'addOAuthService',
|
||||
action: 'Adding_OAuth_Services',
|
||||
});
|
||||
}
|
||||
|
||||
await addOAuthService(name);
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async addOAuthService(name) {
|
||||
methodDeprecationLogger.method('addOAuthService', '9.0.0', '/v1/settings.addCustomOAuth');
|
||||
check(name, String);
|
||||
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addOAuthService' });
|
||||
}
|
||||
|
||||
return addOAuthServiceMethod(userId, name);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { addPermissionToRoleMethod } from '../../lib/authorization/permissionRole';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'authorization:addPermissionToRole'(permissionId: string, role: string): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'authorization:addPermissionToRole'(permissionId, role) {
|
||||
methodDeprecationLogger.method('authorization:addPermissionToRole', '9.0.0', '/v1/permissions.addRole');
|
||||
const uid = Meteor.userId();
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'authorization:addPermissionToRole' });
|
||||
}
|
||||
await addPermissionToRoleMethod(uid, permissionId, role);
|
||||
},
|
||||
});
|
||||
17
apps/meteor/server/meteor-methods/auth/addSamlService.ts
Normal file
17
apps/meteor/server/meteor-methods/auth/addSamlService.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { addSamlService } from '../../lib/saml/lib/settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
addSamlService(name: string): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
addSamlService(name) {
|
||||
addSamlService(name);
|
||||
},
|
||||
});
|
||||
73
apps/meteor/server/meteor-methods/auth/addUserToRole.ts
Normal file
73
apps/meteor/server/meteor-methods/auth/addUserToRole.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { api } from '@rocket.chat/core-services';
|
||||
import type { IRole, IUser } from '@rocket.chat/core-typings';
|
||||
import { Roles, Users } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { addUserRolesAsync } from '../../lib/roles/addUserRoles';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export const addUserToRole = async (userId: string, roleId: string, username: IUser['username'], scope?: string): Promise<boolean> => {
|
||||
if (!(await hasPermissionAsync(userId, 'access-permissions'))) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', {
|
||||
method: 'authorization:addUserToRole',
|
||||
action: 'Accessing_permissions',
|
||||
});
|
||||
}
|
||||
|
||||
if (!roleId || typeof roleId.valueOf() !== 'string' || !username || typeof username.valueOf() !== 'string') {
|
||||
throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', {
|
||||
method: 'authorization:addUserToRole',
|
||||
});
|
||||
}
|
||||
|
||||
const role = await Roles.findOneById<Pick<IRole, '_id'>>(roleId, { projection: { _id: 1 } });
|
||||
|
||||
if (!role) {
|
||||
throw new Meteor.Error('error-invalid-role', 'Invalid Role', {
|
||||
method: 'authorization:addUserToRole',
|
||||
});
|
||||
}
|
||||
|
||||
if (role._id === 'admin' && !(await hasPermissionAsync(userId, 'assign-admin-role'))) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Assigning admin is not allowed', {
|
||||
method: 'authorization:addUserToRole',
|
||||
action: 'Assign_admin',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await Users.findOneByUsernameIgnoringCase(username, {
|
||||
projection: {
|
||||
_id: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user?._id) {
|
||||
throw new Meteor.Error('error-user-not-found', 'User not found', {
|
||||
method: 'authorization:addUserToRole',
|
||||
});
|
||||
}
|
||||
|
||||
// verify if user can be added to given scope
|
||||
if (scope && !(await Roles.canAddUserToRole(user._id, role._id, scope))) {
|
||||
throw new Meteor.Error('error-invalid-user', 'User is not part of given room', {
|
||||
method: 'authorization:addUserToRole',
|
||||
});
|
||||
}
|
||||
|
||||
const add = await addUserRolesAsync(user._id, [role._id], scope);
|
||||
|
||||
if (settings.get('UI_DisplayRoles')) {
|
||||
void api.broadcast('user.roleUpdate', {
|
||||
type: 'added',
|
||||
_id: role._id,
|
||||
u: {
|
||||
_id: user._id,
|
||||
username,
|
||||
},
|
||||
scope,
|
||||
});
|
||||
}
|
||||
|
||||
return add;
|
||||
};
|
||||
28
apps/meteor/server/meteor-methods/auth/afterVerifyEmail.ts
Normal file
28
apps/meteor/server/meteor-methods/auth/afterVerifyEmail.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { runAfterVerifyEmail } from '../../lib/users/runAfterVerifyEmail';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
afterVerifyEmail(): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async afterVerifyEmail() {
|
||||
methodDeprecationLogger.method('afterVerifyEmail', '9.0.0', '/v1/users.verifyEmail');
|
||||
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'afterVerifyEmail',
|
||||
});
|
||||
}
|
||||
|
||||
await runAfterVerifyEmail(userId);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { codesRemainingTotp } from '../../lib/2fa/functions/totp';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'2fa:checkCodesRemaining': () => { remaining: number };
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async '2fa:checkCodesRemaining'() {
|
||||
methodDeprecationLogger.method('2fa:checkCodesRemaining', '9.0.0', '/v1/users.totpCodesRemaining');
|
||||
return codesRemainingTotp(Meteor.userId());
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { settings } from '../../settings';
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
checkRegistrationSecretURL(hash) {
|
||||
check(hash, String);
|
||||
|
||||
return hash === settings.get('Accounts_RegistrationForm_SecretURL');
|
||||
},
|
||||
});
|
||||
32
apps/meteor/server/meteor-methods/auth/createToken.ts
Normal file
32
apps/meteor/server/meteor-methods/auth/createToken.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Authorization, MeteorError, User } from '@rocket.chat/core-services';
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
createToken(userId: string): { userId: string; authToken: string };
|
||||
}
|
||||
}
|
||||
|
||||
const { CREATE_TOKENS_FOR_USERS_SECRET } = process.env;
|
||||
|
||||
export async function generateAccessToken(userId: string, secret: string, caller: IUser) {
|
||||
if (secret !== CREATE_TOKENS_FOR_USERS_SECRET) {
|
||||
throw new MeteorError('error-not-authorized', 'Not authorized');
|
||||
}
|
||||
|
||||
if (caller._id !== userId && !(await Authorization.hasPermission(caller, 'user-generate-access-token'))) {
|
||||
throw new MeteorError('error-not-authorized', 'Not authorized');
|
||||
}
|
||||
|
||||
const token = Accounts._generateStampedLoginToken();
|
||||
await Accounts._insertLoginToken(userId, token);
|
||||
|
||||
await User.ensureLoginTokensLimit(userId);
|
||||
|
||||
return {
|
||||
userId,
|
||||
authToken: token.token,
|
||||
};
|
||||
}
|
||||
81
apps/meteor/server/meteor-methods/auth/crowd.ts
Normal file
81
apps/meteor/server/meteor-methods/auth/crowd.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { CROWD } from '../../lib/auth-providers/crowd/crowd';
|
||||
import { logger } from '../../lib/auth-providers/crowd/logger';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
crowd_test_connection(): { message: TranslationKey; params: string[] };
|
||||
crowd_sync_users(): { message: string; params: string[] };
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async crowd_test_connection() {
|
||||
const user = await Meteor.userAsync();
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'crowd_test_connection',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(user, 'test-admin-options'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'crowd_test_connection',
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.get('CROWD_Enable') !== true) {
|
||||
throw new Meteor.Error('crowd_disabled');
|
||||
}
|
||||
|
||||
try {
|
||||
const crowd = new CROWD();
|
||||
await crowd.checkConnection();
|
||||
|
||||
return {
|
||||
message: 'Crowd_Connection_successful' as const,
|
||||
params: [],
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error({
|
||||
msg: 'Invalid crowd connection details, check the url and application username/password and make sure this server is allowed to speak to crowd',
|
||||
err,
|
||||
});
|
||||
throw new Meteor.Error('Invalid connection details', '', { method: 'crowd_test_connection' });
|
||||
}
|
||||
},
|
||||
async crowd_sync_users() {
|
||||
const user = await Meteor.userAsync();
|
||||
if (settings.get('CROWD_Enable') !== true) {
|
||||
throw new Meteor.Error('crowd_disabled');
|
||||
}
|
||||
|
||||
if (!user || !(await hasPermissionAsync(user, 'sync-auth-services-users'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'crowd_sync_users',
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const crowd = new CROWD();
|
||||
const startTime = Date.now();
|
||||
await crowd.sync();
|
||||
const stopTime = Date.now();
|
||||
const actual = Math.ceil((stopTime - startTime) / 1000);
|
||||
|
||||
return {
|
||||
message: `User data synced in ${actual} seconds`,
|
||||
params: [],
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error({ msg: 'Error syncing user data. ', err });
|
||||
throw new Meteor.Error('Error syncing user data', '', { method: 'crowd_sync_users' });
|
||||
}
|
||||
},
|
||||
});
|
||||
43
apps/meteor/server/meteor-methods/auth/deleteOAuthApp.ts
Normal file
43
apps/meteor/server/meteor-methods/auth/deleteOAuthApp.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { IOAuthApps } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { OAuthAccessTokens, OAuthApps, OAuthAuthCodes } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
deleteOAuthApp(applicationId: IOAuthApps['_id']): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteOAuthApp = async (userId: string, applicationId: IOAuthApps['_id']): Promise<boolean> => {
|
||||
if (!(await hasPermissionAsync(userId, 'manage-oauth-apps'))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'deleteOAuthApp' });
|
||||
}
|
||||
|
||||
const application = await OAuthApps.findOneAndDeleteById(applicationId, { projection: { clientId: 1 } });
|
||||
if (!application) {
|
||||
throw new Meteor.Error('error-application-not-found', 'Application not found', {
|
||||
method: 'deleteOAuthApp',
|
||||
});
|
||||
}
|
||||
|
||||
await OAuthAccessTokens.deleteMany({ clientId: application.clientId });
|
||||
await OAuthAuthCodes.deleteMany({ clientId: application.clientId });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async deleteOAuthApp(applicationId) {
|
||||
methodDeprecationLogger.method('deleteOAuthApp', '9.0.0', '/v1/oauth-apps.delete');
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'deleteOAuthApp' });
|
||||
}
|
||||
|
||||
return deleteOAuthApp(this.userId, applicationId);
|
||||
},
|
||||
});
|
||||
19
apps/meteor/server/meteor-methods/auth/disable.ts
Normal file
19
apps/meteor/server/meteor-methods/auth/disable.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { disableTotp } from '../../lib/2fa/functions/totp';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'2fa:disable': (code: string) => Promise<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async '2fa:disable'(code) {
|
||||
methodDeprecationLogger.method('2fa:disable', '9.0.0', '/v1/users.disableTotp');
|
||||
return disableTotp(Meteor.userId(), code);
|
||||
},
|
||||
});
|
||||
19
apps/meteor/server/meteor-methods/auth/enable.ts
Normal file
19
apps/meteor/server/meteor-methods/auth/enable.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { enableTotp } from '../../lib/2fa/functions/totp';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'2fa:enable': () => Promise<{ secret: string; url: string }>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async '2fa:enable'() {
|
||||
methodDeprecationLogger.method('2fa:enable', '9.0.0', '/v1/users.enableTotp');
|
||||
return enableTotp(Meteor.userId());
|
||||
},
|
||||
});
|
||||
29
apps/meteor/server/meteor-methods/auth/logoutCleanUp.ts
Normal file
29
apps/meteor/server/meteor-methods/auth/logoutCleanUp.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { AppEvents, Apps } from '@rocket.chat/apps';
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { afterLogoutCleanUpCallback } from '../../lib/callbacks/afterLogoutCleanUpCallback';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
logoutCleanUp(user: IUser): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async logoutCleanUp(user) {
|
||||
methodDeprecationLogger.method('logoutCleanUp', '9.0.0', '/v1/users.logout');
|
||||
check(user, Object);
|
||||
|
||||
setImmediate(() => {
|
||||
void afterLogoutCleanUpCallback.run(user);
|
||||
});
|
||||
|
||||
// App IPostUserLogout event hook
|
||||
await Apps.self?.triggerEvent(AppEvents.IPostUserLoggedOut, user);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { refreshLoginServices } from '../../lib/refreshLoginServices';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
refreshOAuthService(): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async refreshOAuthService() {
|
||||
methodDeprecationLogger.method('refreshOAuthService', '9.0.0', '/v1/settings.refreshOAuthServices');
|
||||
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'refreshOAuthService',
|
||||
});
|
||||
}
|
||||
|
||||
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Refresh OAuth Services is not allowed', {
|
||||
method: 'refreshOAuthService',
|
||||
action: 'Refreshing_OAuth_Services',
|
||||
});
|
||||
}
|
||||
|
||||
await refreshLoginServices();
|
||||
},
|
||||
});
|
||||
19
apps/meteor/server/meteor-methods/auth/regenerateCodes.ts
Normal file
19
apps/meteor/server/meteor-methods/auth/regenerateCodes.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { regenerateTotpCodes } from '../../lib/2fa/functions/totp';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'2fa:regenerateCodes': (userToken: string) => { codes: string[] } | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async '2fa:regenerateCodes'(userToken) {
|
||||
methodDeprecationLogger.method('2fa:regenerateCodes', '9.0.0', '/v1/users.regenerateTotpCodes');
|
||||
return regenerateTotpCodes(Meteor.userId(), userToken);
|
||||
},
|
||||
});
|
||||
86
apps/meteor/server/meteor-methods/auth/removeOAuthService.ts
Normal file
86
apps/meteor/server/meteor-methods/auth/removeOAuthService.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Settings } from '@rocket.chat/models';
|
||||
import { capitalize } from '@rocket.chat/tools';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { notifyOnSettingChangedById } from '../../lib/notifyListener';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
removeOAuthService(name: string): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
export const removeCustomOAuthSettings = async (name: string): Promise<void> => {
|
||||
const normalized = capitalize(name.toLowerCase().replace(/[^a-z0-9_]/g, ''));
|
||||
|
||||
if (!normalized) {
|
||||
throw new Meteor.Error('error-invalid-name', 'Invalid OAuth service name', { method: 'removeOAuthService' });
|
||||
}
|
||||
|
||||
const settingsIds = [
|
||||
`Accounts_OAuth_Custom-${normalized}`,
|
||||
`Accounts_OAuth_Custom-${normalized}-url`,
|
||||
`Accounts_OAuth_Custom-${normalized}-token_path`,
|
||||
`Accounts_OAuth_Custom-${normalized}-identity_path`,
|
||||
`Accounts_OAuth_Custom-${normalized}-authorize_path`,
|
||||
`Accounts_OAuth_Custom-${normalized}-scope`,
|
||||
`Accounts_OAuth_Custom-${normalized}-access_token_param`,
|
||||
`Accounts_OAuth_Custom-${normalized}-token_sent_via`,
|
||||
`Accounts_OAuth_Custom-${normalized}-identity_token_sent_via`,
|
||||
`Accounts_OAuth_Custom-${normalized}-id`,
|
||||
`Accounts_OAuth_Custom-${normalized}-secret`,
|
||||
`Accounts_OAuth_Custom-${normalized}-button_label_text`,
|
||||
`Accounts_OAuth_Custom-${normalized}-button_label_color`,
|
||||
`Accounts_OAuth_Custom-${normalized}-button_color`,
|
||||
`Accounts_OAuth_Custom-${normalized}-login_style`,
|
||||
`Accounts_OAuth_Custom-${normalized}-key_field`,
|
||||
`Accounts_OAuth_Custom-${normalized}-username_field`,
|
||||
`Accounts_OAuth_Custom-${normalized}-email_field`,
|
||||
`Accounts_OAuth_Custom-${normalized}-name_field`,
|
||||
`Accounts_OAuth_Custom-${normalized}-avatar_field`,
|
||||
`Accounts_OAuth_Custom-${normalized}-roles_claim`,
|
||||
`Accounts_OAuth_Custom-${normalized}-merge_roles`,
|
||||
`Accounts_OAuth_Custom-${normalized}-roles_to_sync`,
|
||||
`Accounts_OAuth_Custom-${normalized}-merge_users`,
|
||||
`Accounts_OAuth_Custom-${normalized}-show_button`,
|
||||
`Accounts_OAuth_Custom-${normalized}-groups_claim`,
|
||||
`Accounts_OAuth_Custom-${normalized}-channels_admin`,
|
||||
`Accounts_OAuth_Custom-${normalized}-map_channels`,
|
||||
`Accounts_OAuth_Custom-${normalized}-groups_channel_map`,
|
||||
`Accounts_OAuth_Custom-${normalized}-merge_users_distinct_services`,
|
||||
];
|
||||
|
||||
const promises = settingsIds.map((id) => Settings.removeById(id));
|
||||
|
||||
(await Promise.all(promises)).forEach((value, index) => {
|
||||
if (value?.deletedCount) {
|
||||
void notifyOnSettingChangedById(settingsIds[index], 'removed');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async removeOAuthService(name) {
|
||||
methodDeprecationLogger.method('removeOAuthService', '9.0.0', '/v1/settings.removeCustomOAuth');
|
||||
check(name, String);
|
||||
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'removeOAuthService',
|
||||
});
|
||||
}
|
||||
|
||||
if ((await hasPermissionAsync(userId, 'add-oauth-service')) !== true) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'removeOAuthService' });
|
||||
}
|
||||
|
||||
await removeCustomOAuthSettings(name);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { removeRoleFromPermissionMethod } from '../../lib/authorization/permissionRole';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'authorization:removeRoleFromPermission'(permissionId: string, role: string): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'authorization:removeRoleFromPermission'(permissionId, role) {
|
||||
methodDeprecationLogger.method('authorization:removeRoleFromPermission', '9.0.0', '/v1/permissions.removeRole');
|
||||
const uid = Meteor.userId();
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'authorization:removeRoleFromPermission' });
|
||||
}
|
||||
await removeRoleFromPermissionMethod(uid, permissionId, role);
|
||||
},
|
||||
});
|
||||
77
apps/meteor/server/meteor-methods/auth/removeUserFromRole.ts
Normal file
77
apps/meteor/server/meteor-methods/auth/removeUserFromRole.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { api } from '@rocket.chat/core-services';
|
||||
import type { IRole, IUser } from '@rocket.chat/core-typings';
|
||||
import { Roles, Users } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { removeUserFromRolesAsync } from '../../lib/roles/removeUserFromRoles';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export const removeUserFromRole = async (userId: string, roleId: string, username: IUser['username'], scope?: string): Promise<boolean> => {
|
||||
if (!(await hasPermissionAsync(userId, 'access-permissions'))) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Access permissions is not allowed', {
|
||||
method: 'authorization:removeUserFromRole',
|
||||
action: 'Accessing_permissions',
|
||||
});
|
||||
}
|
||||
|
||||
if (!roleId || typeof roleId.valueOf() !== 'string' || !username || typeof username.valueOf() !== 'string') {
|
||||
throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', {
|
||||
method: 'authorization:removeUserFromRole',
|
||||
});
|
||||
}
|
||||
|
||||
const role = await Roles.findOneById<Pick<IRole, '_id'>>(roleId, { projection: { _id: 1 } });
|
||||
if (!role) {
|
||||
throw new Meteor.Error('error-invalid-role', 'Invalid Role', {
|
||||
method: 'authorization:removeUserFromRole',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await Users.findOneByUsernameIgnoringCase(username, {
|
||||
projection: {
|
||||
_id: 1,
|
||||
roles: 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user?._id) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'authorization:removeUserFromRole',
|
||||
});
|
||||
}
|
||||
|
||||
// prevent removing last user from admin role
|
||||
if (role._id === 'admin') {
|
||||
const adminCount = await Users.countDocuments({
|
||||
roles: {
|
||||
$in: ['admin'],
|
||||
},
|
||||
});
|
||||
|
||||
const userIsAdmin = user.roles?.indexOf('admin') > -1;
|
||||
if (adminCount === 1 && userIsAdmin) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Leaving the app without admins is not allowed', {
|
||||
method: 'removeUserFromRole',
|
||||
action: 'Remove_last_admin',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const remove = await removeUserFromRolesAsync(user._id, [role._id], scope);
|
||||
const event = {
|
||||
type: 'removed',
|
||||
_id: role._id,
|
||||
u: {
|
||||
_id: user._id,
|
||||
username,
|
||||
},
|
||||
scope,
|
||||
} as const;
|
||||
if (settings.get('UI_DisplayRoles')) {
|
||||
void api.broadcast('user.roleUpdate', event);
|
||||
}
|
||||
void api.broadcast('federation.userRoleChanged', { ...event, givenByUserId: userId });
|
||||
|
||||
return remove;
|
||||
};
|
||||
78
apps/meteor/server/meteor-methods/auth/samlLogout.ts
Normal file
78
apps/meteor/server/meteor-methods/auth/samlLogout.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import type { IServiceProviderOptions } from '../../lib/saml/definition/IServiceProviderOptions';
|
||||
import { SAMLServiceProvider } from '../../lib/saml/lib/ServiceProvider';
|
||||
import { SAMLUtils } from '../../lib/saml/lib/Utils';
|
||||
|
||||
/**
|
||||
* Fetch SAML provider configs for given 'provider'.
|
||||
*/
|
||||
function getSamlServiceProviderOptions(provider: string): IServiceProviderOptions {
|
||||
if (!provider) {
|
||||
throw new Meteor.Error('no-saml-provider', 'SAML internal error', {
|
||||
method: 'getSamlServiceProviderOptions',
|
||||
});
|
||||
}
|
||||
|
||||
const providers = SAMLUtils.serviceProviders;
|
||||
|
||||
const samlProvider = function (element: IServiceProviderOptions): boolean {
|
||||
return element.provider === provider;
|
||||
};
|
||||
|
||||
return providers.filter(samlProvider)[0];
|
||||
}
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
samlLogout(provider: string): string | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async samlLogout(provider: string) {
|
||||
const userId = Meteor.userId();
|
||||
// Make sure the user is logged in before we initiate SAML Logout
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'samlLogout' });
|
||||
}
|
||||
const providerConfig = getSamlServiceProviderOptions(provider);
|
||||
|
||||
SAMLUtils.log({ msg: 'Logout request', providerConfig });
|
||||
// This query should respect upcoming array of SAML logins
|
||||
const user = await Users.getSAMLByIdAndSAMLProvider(userId, provider);
|
||||
if (!user?.services?.saml) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { nameID, idpSession } = user.services.saml;
|
||||
SAMLUtils.log({ msg: `NameID for user ${Meteor.userId()} found`, nameID });
|
||||
|
||||
const _saml = new SAMLServiceProvider(providerConfig);
|
||||
|
||||
if (!nameID || !idpSession) {
|
||||
SAMLUtils.log({ msg: 'No NameID or idpSession found for user', userId });
|
||||
return;
|
||||
}
|
||||
|
||||
const request = _saml.generateLogoutRequest({
|
||||
nameID: nameID || idpSession,
|
||||
sessionIndex: idpSession,
|
||||
});
|
||||
|
||||
SAMLUtils.log({ request, msg: '----Logout Request---' });
|
||||
|
||||
// request.request: actual XML SAML Request
|
||||
// request.id: comminucation id which will be mentioned in the ResponseTo field of SAMLResponse
|
||||
|
||||
await Users.setSamlInResponseTo(userId, request.id);
|
||||
|
||||
const result = await _saml.requestToUrl(request.request, 'logout');
|
||||
SAMLUtils.log({ msg: 'SAML Logout Request URL generated', result });
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Users } from '@rocket.chat/models';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
export const sendConfirmationEmail = async (to: string): Promise<boolean> => {
|
||||
check(to, String);
|
||||
|
||||
const email = to.trim();
|
||||
|
||||
const user = await Users.findOneByEmailAddress(email, { projection: { emails: 1 } });
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Do not re-send a verification mail to an address that is already verified.
|
||||
const target = user.emails?.find((e) => e.address.toLowerCase() === email.toLowerCase() && !e.verified);
|
||||
if (!target) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
Accounts.sendVerificationEmail(user._id, target.address);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${error.message}`, {
|
||||
method: 'registerUser',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { check } from 'meteor/check';
|
||||
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { SystemLogger } from '../../lib/logger/system';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
sendForgotPasswordEmail(to: string): boolean | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const sendForgotPasswordEmail = async (to: string): Promise<boolean | undefined> => {
|
||||
const email = to.trim().toLowerCase();
|
||||
|
||||
const user = await Users.findOneByEmailAddress(email, { projection: { _id: 1, services: 1 } });
|
||||
|
||||
if (!user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (user.services && !user.services.password) {
|
||||
if (!settings.get('Accounts_AllowPasswordChangeForOAuthUsers')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Accounts.sendResetPasswordEmail(user._id, email);
|
||||
return true;
|
||||
} catch (err) {
|
||||
SystemLogger.error({ err });
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async sendForgotPasswordEmail(to) {
|
||||
check(to, String);
|
||||
|
||||
return sendForgotPasswordEmail(to);
|
||||
},
|
||||
});
|
||||
|
||||
DDPRateLimiter.addRule(
|
||||
{
|
||||
type: 'method',
|
||||
name: 'sendForgotPasswordEmail',
|
||||
clientAddress() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
10,
|
||||
60000,
|
||||
);
|
||||
82
apps/meteor/server/meteor-methods/auth/updateOAuthApp.ts
Normal file
82
apps/meteor/server/meteor-methods/auth/updateOAuthApp.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { IOAuthApps } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { OAuthApps, Users } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { parseUriList } from '../../lib/auth/oauth2-server/parseUriList';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
updateOAuthApp(
|
||||
applicationId: IOAuthApps['_id'],
|
||||
application: Pick<IOAuthApps, 'name' | 'redirectUri' | 'active'>,
|
||||
): Promise<IOAuthApps | null>;
|
||||
}
|
||||
}
|
||||
|
||||
export const updateOAuthApp = async (
|
||||
userId: string,
|
||||
applicationId: IOAuthApps['_id'],
|
||||
application: Pick<IOAuthApps, 'name' | 'redirectUri' | 'active'>,
|
||||
): Promise<IOAuthApps | null> => {
|
||||
if (!(await hasPermissionAsync(userId, 'manage-oauth-apps'))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'updateOAuthApp' });
|
||||
}
|
||||
|
||||
if (!application.name || typeof application.name.valueOf() !== 'string' || application.name.trim() === '') {
|
||||
throw new Meteor.Error('error-invalid-name', 'Invalid name', { method: 'updateOAuthApp' });
|
||||
}
|
||||
|
||||
if (!application.redirectUri || typeof application.redirectUri.valueOf() !== 'string' || application.redirectUri.trim() === '') {
|
||||
throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', {
|
||||
method: 'updateOAuthApp',
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof application.active !== 'boolean') {
|
||||
throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', {
|
||||
method: 'updateOAuthApp',
|
||||
});
|
||||
}
|
||||
|
||||
const redirectUri = parseUriList(application.redirectUri);
|
||||
|
||||
if (redirectUri.length === 0) {
|
||||
throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', {
|
||||
method: 'updateOAuthApp',
|
||||
});
|
||||
}
|
||||
|
||||
const updatedApplication = await OAuthApps.updateById(applicationId, {
|
||||
name: application.name,
|
||||
active: application.active,
|
||||
redirectUri,
|
||||
_updatedBy: await Users.findOneById(userId, {
|
||||
projection: {
|
||||
username: 1,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (updatedApplication == null) {
|
||||
throw new Meteor.Error('error-application-not-found', 'Application not found', {
|
||||
method: 'updateOAuthApp',
|
||||
});
|
||||
}
|
||||
|
||||
return updatedApplication;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async updateOAuthApp(applicationId, application) {
|
||||
methodDeprecationLogger.method('updateOAuthApp', '9.0.0', '/v1/oauth-apps.update');
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateOAuthApp' });
|
||||
}
|
||||
|
||||
return updateOAuthApp(this.userId, applicationId, application);
|
||||
},
|
||||
});
|
||||
20
apps/meteor/server/meteor-methods/auth/validateTempToken.ts
Normal file
20
apps/meteor/server/meteor-methods/auth/validateTempToken.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { validateTotpTempToken } from '../../lib/2fa/functions/totp';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'2fa:validateTempToken': (userToken: string) => { codes: string[] } | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async '2fa:validateTempToken'(userToken) {
|
||||
methodDeprecationLogger.method('2fa:validateTempToken', '9.0.0', '/v1/users.validateTotp');
|
||||
const { 'x-auth-token': xAuthToken } = this.connection?.httpHeaders ?? {};
|
||||
return validateTotpTempToken(Meteor.userId(), userToken, xAuthToken);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import fs from 'node:fs';
|
||||
import type { Readable } from 'node:stream';
|
||||
import { pipeline } from 'node:stream/promises';
|
||||
|
||||
import { Import } from '@rocket.chat/core-services';
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { ProgressStep } from '../../../app/importer/lib/ImporterProgressStep';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { Importers } from '../../lib/import';
|
||||
import { RocketChatImportFileInstance } from '../../lib/import/startup/store';
|
||||
import { SystemLogger } from '../../lib/logger/system';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
async function getHttpFileStream(fileUrl: string): Promise<Readable> {
|
||||
const response = await fetch(fileUrl, {
|
||||
ignoreSsrfValidation: false,
|
||||
allowList: settings.get<string>('SSRF_Allowlist'),
|
||||
});
|
||||
|
||||
const body = response.body as Readable;
|
||||
if (!response.ok) {
|
||||
body.resume();
|
||||
throw new Error(`Unexpected response status ${response.status}`);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
function copyLocalFile(filePath: fs.PathLike, writeStream: fs.WriteStream): void {
|
||||
const readStream = fs.createReadStream(filePath);
|
||||
readStream.pipe(writeStream);
|
||||
}
|
||||
|
||||
export const executeDownloadPublicImportFile = async (userId: IUser['_id'], fileUrl: string, importerKey: string): Promise<void> => {
|
||||
const importer = Importers.get(importerKey);
|
||||
const isUrl = fileUrl.startsWith('http');
|
||||
if (!importer) {
|
||||
throw new Meteor.Error(
|
||||
'error-importer-not-defined',
|
||||
`The importer (${importerKey}) has no import class defined.`,
|
||||
'downloadImportFile',
|
||||
);
|
||||
}
|
||||
// Check if it's a valid url or path before creating a new import record
|
||||
if (!isUrl && !fs.existsSync(fileUrl)) {
|
||||
throw new Meteor.Error('error-import-file-missing', fileUrl, 'downloadPublicImportFile');
|
||||
}
|
||||
|
||||
const operation = await Import.newOperation(userId, importer.name, importer.key);
|
||||
const instance = new importer.importer(importer, operation); // eslint-disable-line new-cap
|
||||
|
||||
const oldFileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1).split('?')[0];
|
||||
const date = new Date();
|
||||
const dateStr = `${date.getUTCFullYear()}${date.getUTCMonth()}${date.getUTCDate()}${date.getUTCHours()}${date.getUTCMinutes()}${date.getUTCSeconds()}`;
|
||||
const newFileName = `${dateStr}_${userId}_${oldFileName}`;
|
||||
|
||||
// Store the file name on the imports collection
|
||||
await instance.startFileUpload(newFileName);
|
||||
await instance.updateProgress(ProgressStep.DOWNLOADING_FILE);
|
||||
|
||||
const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName);
|
||||
let errorProgressUpdate: Promise<unknown> | undefined;
|
||||
const markImportAsFailed = (): Promise<unknown> => {
|
||||
errorProgressUpdate ??= instance.updateProgress(ProgressStep.ERROR).catch((error) => {
|
||||
SystemLogger.error({ msg: 'Failed to update import progress to ERROR', err: error });
|
||||
});
|
||||
return errorProgressUpdate;
|
||||
};
|
||||
|
||||
writeStream.on('error', () => {
|
||||
void markImportAsFailed();
|
||||
});
|
||||
|
||||
let readStream: Readable | undefined;
|
||||
if (isUrl) {
|
||||
try {
|
||||
readStream = await getHttpFileStream(fileUrl);
|
||||
} catch (error) {
|
||||
writeStream.destroy();
|
||||
await markImportAsFailed();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
writeStream.on('finish', () => {
|
||||
void instance.updateProgress(ProgressStep.FILE_LOADED);
|
||||
});
|
||||
|
||||
if (readStream) {
|
||||
void pipeline(readStream, writeStream).catch(() => markImportAsFailed());
|
||||
return;
|
||||
}
|
||||
|
||||
// If the url is actually a folder path on the current machine, skip moving it to the file store
|
||||
if (fs.statSync(fileUrl).isDirectory()) {
|
||||
await instance.updateRecord({ file: fileUrl });
|
||||
await instance.updateProgress(ProgressStep.FILE_LOADED);
|
||||
return;
|
||||
}
|
||||
|
||||
copyLocalFile(fileUrl, writeStream);
|
||||
};
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
downloadPublicImportFile(fileUrl: string, importerKey: string): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async downloadPublicImportFile(fileUrl: string, importerKey: string) {
|
||||
methodDeprecationLogger.method('downloadPublicImportFile', '9.0.0', '/v1/downloadPublicImportFile');
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', 'downloadPublicImportFile');
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(userId, 'run-import'))) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', 'downloadPublicImportFile');
|
||||
}
|
||||
|
||||
await executeDownloadPublicImportFile(userId, fileUrl, importerKey);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import type { IImportProgress, IImporterSelection } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Imports } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { ProgressStep } from '../../../app/importer/lib/ImporterProgressStep';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { Importers } from '../../lib/import';
|
||||
import { RocketChatImportFileInstance } from '../../lib/import/startup/store';
|
||||
|
||||
export const executeGetImportFileData = async (): Promise<IImporterSelection | { waiting: true }> => {
|
||||
const operation = await Imports.findLastImport();
|
||||
if (!operation) {
|
||||
throw new Meteor.Error('error-operation-not-found', 'Import Operation Not Found', 'getImportFileData');
|
||||
}
|
||||
|
||||
const { importerKey } = operation;
|
||||
|
||||
const importer = Importers.get(importerKey);
|
||||
if (!importer) {
|
||||
throw new Meteor.Error('error-importer-not-defined', `The importer (${importerKey}) has no import class defined.`, 'getImportFileData');
|
||||
}
|
||||
|
||||
const instance = new importer.importer(importer, operation); // eslint-disable-line new-cap
|
||||
|
||||
const waitingSteps: IImportProgress['step'][] = [
|
||||
ProgressStep.DOWNLOADING_FILE,
|
||||
ProgressStep.PREPARING_CHANNELS,
|
||||
ProgressStep.PREPARING_MESSAGES,
|
||||
ProgressStep.PREPARING_USERS,
|
||||
ProgressStep.PREPARING_CONTACTS,
|
||||
ProgressStep.PREPARING_STARTED,
|
||||
];
|
||||
|
||||
if (waitingSteps.indexOf(instance.progress.step) >= 0) {
|
||||
if (instance.importRecord?.valid) {
|
||||
return { waiting: true };
|
||||
}
|
||||
throw new Meteor.Error('error-import-operation-invalid', 'Invalid Import Operation', 'getImportFileData');
|
||||
}
|
||||
|
||||
const readySteps: IImportProgress['step'][] = [
|
||||
ProgressStep.USER_SELECTION,
|
||||
ProgressStep.DONE,
|
||||
ProgressStep.CANCELLED,
|
||||
ProgressStep.ERROR,
|
||||
];
|
||||
|
||||
if (readySteps.indexOf(instance.progress.step) >= 0) {
|
||||
return instance.buildSelection();
|
||||
}
|
||||
|
||||
const fileName = instance.importRecord.file;
|
||||
if (fileName) {
|
||||
const fullFilePath = fs.existsSync(fileName) ? fileName : path.join(RocketChatImportFileInstance.absolutePath, fileName);
|
||||
await instance.prepareUsingLocalFile(fullFilePath);
|
||||
}
|
||||
|
||||
return instance.buildSelection();
|
||||
};
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getImportFileData(): IImporterSelection | { waiting: true };
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getImportFileData() {
|
||||
methodDeprecationLogger.method('getImportFileData', '9.0.0', '/v1/getImportFileData');
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', 'getImportFileData');
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(userId, 'run-import'))) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', 'getImportFileData');
|
||||
}
|
||||
|
||||
return executeGetImportFileData();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { IImportProgress } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Imports } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { Importers } from '../../lib/import';
|
||||
|
||||
export const executeGetImportProgress = async (): Promise<IImportProgress> => {
|
||||
const operation = await Imports.findLastImport();
|
||||
if (!operation) {
|
||||
throw new Meteor.Error('error-operation-not-found', 'Import Operation Not Found', 'getImportProgress');
|
||||
}
|
||||
|
||||
const { importerKey } = operation;
|
||||
const importer = Importers.get(importerKey);
|
||||
if (!importer) {
|
||||
throw new Meteor.Error('error-importer-not-defined', `The importer (${importerKey}) has no import class defined.`, 'getImportProgress');
|
||||
}
|
||||
|
||||
const instance = new importer.importer(importer, operation); // eslint-disable-line new-cap
|
||||
|
||||
return instance.getProgress();
|
||||
};
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getImportProgress(): IImportProgress;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getImportProgress() {
|
||||
methodDeprecationLogger.method('getImportProgress', '9.0.0', '/v1/getImportProgress');
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', 'getImportProgress');
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(userId, 'run-import'))) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', 'setupImporter');
|
||||
}
|
||||
|
||||
return executeGetImportProgress();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { IImport } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Imports } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
export const executeGetLatestImportOperations = async () => {
|
||||
const data = Imports.find(
|
||||
{},
|
||||
{
|
||||
sort: { _updatedAt: -1 },
|
||||
limit: 20,
|
||||
},
|
||||
);
|
||||
|
||||
return data.toArray();
|
||||
};
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getLatestImportOperations(): IImport[];
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getLatestImportOperations() {
|
||||
methodDeprecationLogger.method('getLatestImportOperations', '9.0.0', '/v1/getLatestImportOperations');
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', 'getLatestImportOperations');
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(userId, 'view-import-operations'))) {
|
||||
throw new Meteor.Error('not_authorized', 'User not authorized', 'getLatestImportOperations');
|
||||
}
|
||||
|
||||
return executeGetLatestImportOperations();
|
||||
},
|
||||
});
|
||||
54
apps/meteor/server/meteor-methods/import/startImport.ts
Normal file
54
apps/meteor/server/meteor-methods/import/startImport.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Imports } from '@rocket.chat/models';
|
||||
import { isStartImportParamsPOST, type StartImportParamsPOST } from '@rocket.chat/rest-typings';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { Importers } from '../../lib/import';
|
||||
|
||||
export const executeStartImport = async ({ input }: StartImportParamsPOST, startedByUserId: IUser['_id']) => {
|
||||
const operation = await Imports.findLastImport();
|
||||
if (!operation) {
|
||||
throw new Meteor.Error('error-operation-not-found', 'Import Operation Not Found', 'startImport');
|
||||
}
|
||||
|
||||
const { importerKey } = operation;
|
||||
const importer = Importers.get(importerKey);
|
||||
if (!importer) {
|
||||
throw new Meteor.Error('error-importer-not-defined', `The importer (${importerKey}) has no import class defined.`, 'startImport');
|
||||
}
|
||||
|
||||
const instance = new importer.importer(importer, operation); // eslint-disable-line new-cap
|
||||
|
||||
await instance.startImport(input, startedByUserId);
|
||||
};
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
startImport(params: StartImportParamsPOST): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async startImport({ input }: StartImportParamsPOST) {
|
||||
methodDeprecationLogger.method('startImport', '9.0.0', '/v1/startImport');
|
||||
if (!input || typeof input !== 'object' || !isStartImportParamsPOST({ input })) {
|
||||
throw new Meteor.Error(`Invalid Selection data provided to the importer.`);
|
||||
}
|
||||
|
||||
const userId = Meteor.userId();
|
||||
// Takes name and object with users / channels selected to import
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', 'startImport');
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(userId, 'run-import'))) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', 'startImport');
|
||||
}
|
||||
|
||||
return executeStartImport({ input }, userId);
|
||||
},
|
||||
});
|
||||
81
apps/meteor/server/meteor-methods/import/uploadImportFile.ts
Normal file
81
apps/meteor/server/meteor-methods/import/uploadImportFile.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Import } from '@rocket.chat/core-services';
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { ProgressStep } from '../../../app/importer/lib/ImporterProgressStep';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { Importers } from '../../lib/import';
|
||||
import { RocketChatImportFileInstance } from '../../lib/import/startup/store';
|
||||
import { RocketChatFile } from '../../lib/media/file';
|
||||
|
||||
export const executeUploadImportFile = async (
|
||||
userId: IUser['_id'],
|
||||
binaryContent: string,
|
||||
contentType: string,
|
||||
fileName: string,
|
||||
importerKey: string,
|
||||
): Promise<void> => {
|
||||
const importer = Importers.get(importerKey);
|
||||
if (!importer) {
|
||||
throw new Meteor.Error('error-importer-not-defined', `The importer (${importerKey}) has no import class defined.`, 'uploadImportFile');
|
||||
}
|
||||
|
||||
const operation = await Import.newOperation(userId, importer.name, importer.key);
|
||||
|
||||
const instance = new importer.importer(importer, operation); // eslint-disable-line new-cap
|
||||
|
||||
const date = new Date();
|
||||
const dateStr = `${date.getUTCFullYear()}${date.getUTCMonth()}${date.getUTCDate()}${date.getUTCHours()}${date.getUTCMinutes()}${date.getUTCSeconds()}`;
|
||||
const newFileName = `${dateStr}_${userId}_${fileName}`;
|
||||
|
||||
// Store the file name and content type on the imports collection
|
||||
await instance.startFileUpload(newFileName, contentType);
|
||||
|
||||
// Save the file on the File Store
|
||||
const file = Buffer.from(binaryContent, 'base64');
|
||||
const readStream = RocketChatFile.bufferToStream(file);
|
||||
const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName, contentType);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
try {
|
||||
writeStream.on('end', () => {
|
||||
resolve();
|
||||
});
|
||||
writeStream.on('error', (e: Error) => {
|
||||
reject(e);
|
||||
});
|
||||
|
||||
readStream.pipe(writeStream);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
await instance.updateProgress(ProgressStep.FILE_LOADED);
|
||||
};
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
uploadImportFile(binaryContent: string, contentType: string, fileName: string, importerKey: string): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async uploadImportFile(binaryContent, contentType, fileName, importerKey) {
|
||||
methodDeprecationLogger.method('uploadImportFile', '9.0.0', '/v1/uploadImportFile');
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', 'uploadImportFile');
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(userId, 'run-import'))) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Importing is not allowed', 'uploadImportFile');
|
||||
}
|
||||
|
||||
await executeUploadImportFile(userId, binaryContent, contentType, fileName, importerKey);
|
||||
},
|
||||
});
|
||||
148
apps/meteor/server/meteor-methods/index.ts
Normal file
148
apps/meteor/server/meteor-methods/index.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import '../../imports/personal-access-tokens/server/api/methods';
|
||||
|
||||
import './auth/addOAuthService';
|
||||
import './auth/addPermissionToRole';
|
||||
import './auth/addSamlService';
|
||||
import './auth/addUserToRole';
|
||||
import './auth/afterVerifyEmail';
|
||||
import './auth/checkCodesRemaining';
|
||||
import './auth/checkRegistrationSecretURL';
|
||||
import './auth/crowd';
|
||||
import './auth/disable';
|
||||
import './auth/enable';
|
||||
import './auth/logoutCleanUp';
|
||||
import './auth/refreshOAuthService';
|
||||
import './auth/regenerateCodes';
|
||||
import './auth/removeOAuthService';
|
||||
import './auth/removeRoleFromPermission';
|
||||
import './auth/samlLogout';
|
||||
import './auth/sendConfirmationEmail';
|
||||
import './auth/sendForgotPasswordEmail';
|
||||
import './auth/validateTempToken';
|
||||
import './import/downloadPublicImportFile';
|
||||
import './import/getImportFileData';
|
||||
import './import/getImportProgress';
|
||||
import './import/getLatestImportOperations';
|
||||
import './import/startImport';
|
||||
import './import/uploadImportFile';
|
||||
import './integrations/clearIntegrationHistory';
|
||||
import './integrations/incoming/addIncomingIntegration';
|
||||
import './integrations/incoming/deleteIncomingIntegration';
|
||||
import './integrations/incoming/updateIncomingIntegration';
|
||||
import './integrations/outgoing/addOutgoingIntegration';
|
||||
import './integrations/outgoing/deleteOutgoingIntegration';
|
||||
import './integrations/outgoing/replayOutgoingIntegration';
|
||||
import './integrations/outgoing/updateOutgoingIntegration';
|
||||
import './media/deleteCustomSound';
|
||||
import './media/deleteEmojiCustom';
|
||||
import './media/getS3FileUrl';
|
||||
import './media/insertOrUpdateEmoji';
|
||||
import './media/insertOrUpdateSound';
|
||||
import './media/listCustomSounds';
|
||||
import './media/uploadCustomSound';
|
||||
import './media/uploadEmojiCustom';
|
||||
import './messages/createDirectMessage';
|
||||
import './messages/createDiscussion';
|
||||
import './messages/deleteFileMessage';
|
||||
import './messages/executeSlashCommandPreview';
|
||||
import './messages/followMessage';
|
||||
import './messages/getChannelHistory';
|
||||
import './messages/getMessages';
|
||||
import './messages/getSingleMessage';
|
||||
import './messages/getSlashCommandPreviews';
|
||||
import './messages/getUserMentionsByChannel';
|
||||
import './messages/getThreadMessages';
|
||||
import './messages/getThreadsList';
|
||||
import './messages/loadHistory';
|
||||
import './messages/loadMissedMessages';
|
||||
import './messages/loadNextMessages';
|
||||
import './messages/loadSurroundingMessages';
|
||||
import './messages/messageSearch';
|
||||
import './messages/readMessages';
|
||||
import './messages/readThreads';
|
||||
import './messages/sendFileMessage';
|
||||
import './messages/sendMessage';
|
||||
import './messages/unfollowMessage';
|
||||
import './messages/updateMessage';
|
||||
import './omnichannel/sendFileLivechatMessage';
|
||||
import './omnichannel/sendMessageLivechat';
|
||||
import './platform/OEmbedCacheCleanup';
|
||||
import './platform/banner_dismiss';
|
||||
import './platform/checkFederationConfiguration';
|
||||
import './platform/cloud';
|
||||
import './platform/fetchMyKeys';
|
||||
import './platform/getProviderUiMetadata';
|
||||
import './platform/getStatistics';
|
||||
import './platform/getSupportedLanguages';
|
||||
import './platform/getUsersOfRoomWithoutKey';
|
||||
import './platform/loadLocale';
|
||||
import './platform/push';
|
||||
import './platform/requestDataDownload';
|
||||
import './platform/requestSubscriptionKeys';
|
||||
import './platform/resetOwnE2EKey';
|
||||
import './platform/restartServer';
|
||||
import './platform/search';
|
||||
import './platform/saveSettings';
|
||||
import './platform/setRoomKeyID';
|
||||
import './platform/setUserPublicAndPrivateKeys';
|
||||
import './platform/translateMessage';
|
||||
import './platform/updateGroupKey';
|
||||
import './rooms/addAllUserToRoom';
|
||||
import './rooms/addRoomLeader';
|
||||
import './rooms/addRoomModerator';
|
||||
import './rooms/addRoomOwner';
|
||||
import './rooms/addUserToRoom';
|
||||
import './rooms/addUsersToRoom';
|
||||
import './rooms/archiveRoom';
|
||||
import './rooms/browseChannels';
|
||||
import './rooms/channelsList';
|
||||
import './rooms/cleanRoomHistory';
|
||||
import './rooms/createChannel';
|
||||
import './rooms/createPrivateGroup';
|
||||
import './rooms/getRoomById';
|
||||
import './rooms/getRoomIdByNameOrId';
|
||||
import './rooms/getRoomJoinCode';
|
||||
import './rooms/getRoomNameById';
|
||||
import './rooms/getRoomRoles';
|
||||
import './rooms/getTotalChannels';
|
||||
import './rooms/hideRoom';
|
||||
import './rooms/joinDefaultChannels';
|
||||
import './rooms/joinRoom';
|
||||
import './rooms/leaveRoom';
|
||||
import './rooms/muteUserInRoom';
|
||||
import './rooms/openRoom';
|
||||
import './rooms/removeRoomLeader';
|
||||
import './rooms/removeRoomModerator';
|
||||
import './rooms/removeRoomOwner';
|
||||
import './rooms/removeUserFromRoom';
|
||||
import './rooms/saveRoomSettings';
|
||||
import './rooms/toggleFavorite';
|
||||
import './rooms/unarchiveRoom';
|
||||
import './rooms/unmuteUserInRoom';
|
||||
import './settings/getSetupWizardParameters';
|
||||
import './settings/saveSetting';
|
||||
import './settings/saveSettings';
|
||||
import './settings/sendSMTPTestEmail';
|
||||
import './users/blockUser';
|
||||
import './users/deleteCustomUserStatus';
|
||||
import './users/deleteUser';
|
||||
import './users/deleteUserOwnAccount';
|
||||
import './users/getUsernameSuggestion';
|
||||
import './users/getUsersOfRoom';
|
||||
import './users/getUserStatusText';
|
||||
import './users/ignoreUser';
|
||||
import './users/insertOrUpdateUserStatus';
|
||||
import './users/listCustomUserStatus';
|
||||
import './users/registerUser';
|
||||
import './users/resetAvatar';
|
||||
import './users/saveNotificationSettings';
|
||||
import './users/saveUserPreferences';
|
||||
import './users/saveUserProfile';
|
||||
import './users/setAvatarFromService';
|
||||
import './users/setEmail';
|
||||
import './users/setRealName';
|
||||
import './users/setUserActiveStatus';
|
||||
import './users/setUserStatus';
|
||||
import './users/unblockUser';
|
||||
import './users/userPresence';
|
||||
import './users/userSetUtcOffset';
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { clearIntegrationHistoryMethod } from '../../lib/integrations/functions/clearIntegrationHistory';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
clearIntegrationHistory(integrationId: string): Promise<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async clearIntegrationHistory(integrationId) {
|
||||
methodDeprecationLogger.method('clearIntegrationHistory', '9.0.0', '/v1/integrations.clearHistory');
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'clearIntegrationHistory' });
|
||||
}
|
||||
await clearIntegrationHistoryMethod(this.userId, integrationId);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
import type { INewIncomingIntegration, IIncomingIntegration } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Integrations, Subscriptions, Users, Rooms } from '@rocket.chat/models';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import { removeEmpty } from '@rocket.chat/tools';
|
||||
import { Match, check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync, hasAllPermissionAsync } from '../../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../../lib/deprecationWarningLogger';
|
||||
import { compileIntegrationScript } from '../../../lib/integrations/lib/compileIntegrationScript';
|
||||
import { validateScriptEngine, isScriptEngineFrozen } from '../../../lib/integrations/lib/validateScriptEngine';
|
||||
import { notifyOnIntegrationChanged } from '../../../lib/notifyListener';
|
||||
|
||||
const validChannelChars = ['@', '#'];
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
addIncomingIntegration(integration: INewIncomingIntegration): Promise<IIncomingIntegration>;
|
||||
}
|
||||
}
|
||||
|
||||
export const addIncomingIntegration = async (userId: string, integration: INewIncomingIntegration): Promise<IIncomingIntegration> => {
|
||||
check(
|
||||
integration,
|
||||
Match.ObjectIncluding({
|
||||
type: String,
|
||||
name: String,
|
||||
enabled: Boolean,
|
||||
username: String,
|
||||
channel: String,
|
||||
alias: Match.Maybe(String),
|
||||
emoji: Match.Maybe(String),
|
||||
scriptEnabled: Boolean,
|
||||
scriptEngine: Match.Maybe(String),
|
||||
overrideDestinationChannelEnabled: Match.Maybe(Boolean),
|
||||
script: Match.Maybe(String),
|
||||
avatar: Match.Maybe(String),
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
!userId ||
|
||||
(!(await hasPermissionAsync(userId, 'manage-incoming-integrations')) &&
|
||||
!(await hasPermissionAsync(userId, 'manage-own-incoming-integrations')))
|
||||
) {
|
||||
throw new Meteor.Error('not_authorized', 'Unauthorized', {
|
||||
method: 'addIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
if (!integration.channel || typeof integration.channel.valueOf() !== 'string') {
|
||||
throw new Meteor.Error('error-invalid-channel', 'Invalid channel', {
|
||||
method: 'addIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
if (integration.channel.trim() === '') {
|
||||
throw new Meteor.Error('error-invalid-channel', 'Invalid channel', {
|
||||
method: 'addIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
const channels = integration.channel.split(',').map((channel) => channel.trim());
|
||||
|
||||
for (const channel of channels) {
|
||||
if (!validChannelChars.includes(channel[0])) {
|
||||
throw new Meteor.Error('error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', {
|
||||
method: 'updateIncomingIntegration',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!integration.username || typeof integration.username.valueOf() !== 'string' || integration.username.trim() === '') {
|
||||
throw new Meteor.Error('error-invalid-username', 'Invalid username', {
|
||||
method: 'addIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
if (integration.script?.trim()) {
|
||||
validateScriptEngine(integration.scriptEngine ?? 'isolated-vm');
|
||||
}
|
||||
|
||||
const user = await Users.findOneByUsername(integration.username, { projection: { _id: 1 } });
|
||||
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(user._id, 'message-impersonate'))) {
|
||||
throw new Meteor.Error(
|
||||
'error-user-lacks-message-impersonate-permission',
|
||||
"User selected for the incoming integration lacks the 'message-impersonate' permission.",
|
||||
{
|
||||
method: 'addIncomingIntegration',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Default to transpiling with Babel for backwards compatibility; integrations
|
||||
// can opt-out per-record by setting `skipTranspile: true` (removed in 9.0.0).
|
||||
const skipTranspile = integration.skipTranspile === true;
|
||||
|
||||
const integrationData: IIncomingIntegration = {
|
||||
...integration,
|
||||
scriptEngine: integration.scriptEngine ?? 'isolated-vm',
|
||||
skipTranspile,
|
||||
type: 'webhook-incoming',
|
||||
channel: channels,
|
||||
overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled ?? false,
|
||||
token: Random.id(48),
|
||||
userId: user._id,
|
||||
_createdAt: new Date(),
|
||||
_createdBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }),
|
||||
};
|
||||
|
||||
if (
|
||||
!isScriptEngineFrozen(integrationData.scriptEngine) &&
|
||||
integration.scriptEnabled === true &&
|
||||
integration.script &&
|
||||
integration.script.trim() !== ''
|
||||
) {
|
||||
const { script, error } = compileIntegrationScript(integration.script, { transpile: !skipTranspile });
|
||||
if (error) {
|
||||
integrationData.scriptCompiled = undefined;
|
||||
integrationData.scriptError = error;
|
||||
} else {
|
||||
integrationData.scriptCompiled = script;
|
||||
delete integrationData.scriptError;
|
||||
}
|
||||
}
|
||||
|
||||
for (let channel of channels) {
|
||||
let record;
|
||||
const channelType = channel[0];
|
||||
channel = channel.substr(1);
|
||||
|
||||
switch (channelType) {
|
||||
case '#':
|
||||
record = await Rooms.findOne({
|
||||
$or: [{ _id: channel }, { name: channel }],
|
||||
});
|
||||
break;
|
||||
case '@':
|
||||
record = await Users.findOne({
|
||||
$or: [{ _id: channel }, { username: channel }],
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (!record) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', {
|
||||
method: 'addIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!(await hasAllPermissionAsync(userId, ['manage-incoming-integrations', 'manage-own-incoming-integrations'])) &&
|
||||
!(await Subscriptions.findOneByRoomIdAndUserId(record._id, userId, { projection: { _id: 1 } }))
|
||||
) {
|
||||
throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', {
|
||||
method: 'addIncomingIntegration',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const strippedIntegrationData = removeEmpty(integrationData);
|
||||
|
||||
const { insertedId } = await Integrations.insertOne(strippedIntegrationData);
|
||||
|
||||
const integrationStored = await Integrations.findOne({ _id: insertedId });
|
||||
|
||||
if (!integrationStored) {
|
||||
throw new Error('Error inserting integration');
|
||||
}
|
||||
void notifyOnIntegrationChanged({ ...integrationStored, _id: insertedId }, 'inserted');
|
||||
|
||||
return integrationStored as IIncomingIntegration;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async addIncomingIntegration(integration: INewIncomingIntegration): Promise<IIncomingIntegration> {
|
||||
methodDeprecationLogger.method('addIncomingIntegration', '9.0.0', '/v1/integrations.create');
|
||||
const { userId } = this;
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('invalid-user', 'Invalid User', {
|
||||
method: 'addIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
return addIncomingIntegration(userId, integration);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Integrations } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../../lib/deprecationWarningLogger';
|
||||
import { notifyOnIntegrationChanged } from '../../../lib/notifyListener';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
deleteIncomingIntegration(integrationId: string): Promise<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteIncomingIntegration = async (integrationId: string, userId: string): Promise<void> => {
|
||||
const canManageAllIntegrations = !!userId && (await hasPermissionAsync(userId, 'manage-incoming-integrations'));
|
||||
const canManageOwnIntegrations =
|
||||
!canManageAllIntegrations && !!userId && (await hasPermissionAsync(userId, 'manage-own-incoming-integrations'));
|
||||
|
||||
if (!canManageAllIntegrations && !canManageOwnIntegrations) {
|
||||
throw new Meteor.Error('not_authorized', 'Unauthorized', {
|
||||
method: 'deleteIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
const integration = await Integrations.removeByIdAndCreatedByIfExists({
|
||||
_id: integrationId,
|
||||
...(canManageOwnIntegrations && { createdBy: userId }),
|
||||
});
|
||||
|
||||
if (!integration) {
|
||||
throw new Meteor.Error('error-invalid-integration', 'Invalid integration', {
|
||||
method: 'deleteIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
void notifyOnIntegrationChanged(integration, 'removed');
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async deleteIncomingIntegration(integrationId) {
|
||||
methodDeprecationLogger.method('deleteIncomingIntegration', '9.0.0', '/v1/integrations.remove');
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('not_authorized', 'Unauthorized', {
|
||||
method: 'deleteIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
await deleteIncomingIntegration(integrationId, userId);
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,211 @@
|
||||
import type { IIntegration, INewIncomingIntegration, IUpdateIncomingIntegration } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Integrations, Subscriptions, Users, Rooms } from '@rocket.chat/models';
|
||||
import { wrapExceptions } from '@rocket.chat/tools';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasAllPermissionAsync, hasPermissionAsync } from '../../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../../lib/deprecationWarningLogger';
|
||||
import { compileIntegrationScript } from '../../../lib/integrations/lib/compileIntegrationScript';
|
||||
import { isScriptEngineFrozen, validateScriptEngine } from '../../../lib/integrations/lib/validateScriptEngine';
|
||||
import { notifyOnIntegrationChanged } from '../../../lib/notifyListener';
|
||||
|
||||
const validChannelChars = ['@', '#'];
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
updateIncomingIntegration(
|
||||
integrationId: string,
|
||||
integration: INewIncomingIntegration | IUpdateIncomingIntegration,
|
||||
): IIntegration | null;
|
||||
}
|
||||
}
|
||||
|
||||
function validateChannels(channelString: string | undefined): string[] {
|
||||
if (!channelString || typeof channelString.valueOf() !== 'string' || channelString.trim() === '') {
|
||||
throw new Meteor.Error('error-invalid-channel', 'Invalid channel', {
|
||||
method: 'updateIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
const channels = channelString.split(',').map((channel) => channel.trim());
|
||||
|
||||
for (const channel of channels) {
|
||||
if (!validChannelChars.includes(channel[0])) {
|
||||
throw new Meteor.Error('error-invalid-channel-start-with-chars', 'Invalid channel. Start with @ or #', {
|
||||
method: 'updateIncomingIntegration',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
export const updateIncomingIntegration = async (
|
||||
userId: string,
|
||||
integrationId: string,
|
||||
integration: INewIncomingIntegration | IUpdateIncomingIntegration,
|
||||
): Promise<IIntegration | null> => {
|
||||
const channels = validateChannels(integration.channel);
|
||||
|
||||
let currentIntegration;
|
||||
|
||||
if (await hasPermissionAsync(userId, 'manage-incoming-integrations')) {
|
||||
currentIntegration = await Integrations.findOneById(integrationId);
|
||||
} else if (await hasPermissionAsync(userId, 'manage-own-incoming-integrations')) {
|
||||
currentIntegration = await Integrations.findOne({
|
||||
'_id': integrationId,
|
||||
'_createdBy._id': userId,
|
||||
});
|
||||
} else {
|
||||
throw new Meteor.Error('not_authorized', 'Unauthorized', {
|
||||
method: 'updateIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
if (!currentIntegration) {
|
||||
throw new Meteor.Error('error-invalid-integration', 'Invalid integration', {
|
||||
method: 'updateIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
const oldScriptEngine = currentIntegration.scriptEngine;
|
||||
const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm';
|
||||
if (
|
||||
integration.script?.trim() &&
|
||||
(scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim())
|
||||
) {
|
||||
wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => {
|
||||
throw new Meteor.Error(e.message);
|
||||
});
|
||||
}
|
||||
|
||||
const isFrozen = isScriptEngineFrozen(scriptEngine);
|
||||
|
||||
// Default to transpiling with Babel for backwards compatibility; integrations
|
||||
// can opt-out per-record by setting `skipTranspile: true` (removed in 9.0.0).
|
||||
const skipTranspile = integration.skipTranspile === true;
|
||||
|
||||
if (!isFrozen && integration.scriptEnabled === true && integration.script && integration.script.trim() !== '') {
|
||||
const { script, error } = compileIntegrationScript(integration.script, { transpile: !skipTranspile });
|
||||
if (error) {
|
||||
await Integrations.updateOne(
|
||||
{ _id: integrationId },
|
||||
{
|
||||
$set: { scriptError: error, skipTranspile },
|
||||
$unset: { scriptCompiled: 1 as const },
|
||||
},
|
||||
);
|
||||
} else {
|
||||
await Integrations.updateOne(
|
||||
{ _id: integrationId },
|
||||
{
|
||||
$set: { scriptCompiled: script, skipTranspile },
|
||||
$unset: { scriptError: 1 as const },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (let channel of channels) {
|
||||
const channelType = channel[0];
|
||||
channel = channel.slice(1);
|
||||
let record;
|
||||
|
||||
switch (channelType) {
|
||||
case '#':
|
||||
record = await Rooms.findOne({
|
||||
$or: [{ _id: channel }, { name: channel }],
|
||||
});
|
||||
break;
|
||||
case '@':
|
||||
record = await Users.findOne({
|
||||
$or: [{ _id: channel }, { username: channel }],
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (!record) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', {
|
||||
method: 'updateIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!(await hasAllPermissionAsync(userId, ['manage-incoming-integrations', 'manage-own-incoming-integrations'])) &&
|
||||
!(await Subscriptions.findOneByRoomIdAndUserId(record._id, userId, { projection: { _id: 1 } }))
|
||||
) {
|
||||
throw new Meteor.Error('error-invalid-channel', 'Invalid Channel', {
|
||||
method: 'updateIncomingIntegration',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const username = 'username' in integration ? integration.username : currentIntegration.username;
|
||||
const user = await Users.findOneByUsername(username, { projection: { _id: 1, username: 1 } });
|
||||
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', {
|
||||
method: 'updateIncomingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(user._id, 'message-impersonate'))) {
|
||||
throw new Meteor.Error(
|
||||
'error-user-lacks-message-impersonate-permission',
|
||||
"User selected for the incoming integration lacks the 'message-impersonate' permission.",
|
||||
{
|
||||
method: 'updateIncomingIntegration',
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const updatedIntegration = await Integrations.findOneAndUpdate(
|
||||
{ _id: integrationId },
|
||||
{
|
||||
$set: {
|
||||
enabled: integration.enabled,
|
||||
name: integration.name,
|
||||
...(typeof integration.avatar !== 'undefined' && { avatar: integration.avatar }),
|
||||
...(typeof integration.emoji !== 'undefined' && { emoji: integration.emoji }),
|
||||
...(typeof integration.alias !== 'undefined' && { alias: integration.alias }),
|
||||
...(channels && { channel: channels }),
|
||||
...('username' in integration && { username: user.username, userId: user._id }),
|
||||
...(isFrozen
|
||||
? {}
|
||||
: {
|
||||
...(typeof integration.script !== 'undefined' && { script: integration.script }),
|
||||
scriptEnabled: integration.scriptEnabled,
|
||||
...(scriptEngine && { scriptEngine }),
|
||||
skipTranspile,
|
||||
}),
|
||||
...(typeof integration.overrideDestinationChannelEnabled !== 'undefined' && {
|
||||
overrideDestinationChannelEnabled: integration.overrideDestinationChannelEnabled,
|
||||
}),
|
||||
_updatedAt: new Date(),
|
||||
_updatedBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }),
|
||||
},
|
||||
},
|
||||
{ returnDocument: 'after' },
|
||||
);
|
||||
|
||||
if (updatedIntegration) {
|
||||
void notifyOnIntegrationChanged(updatedIntegration);
|
||||
}
|
||||
|
||||
return updatedIntegration;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async updateIncomingIntegration(integrationId, integration) {
|
||||
methodDeprecationLogger.method('updateIncomingIntegration', '9.0.0', '/v1/integrations.update');
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'updateOutgoingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
return updateIncomingIntegration(this.userId, integrationId, integration);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { INewOutgoingIntegration, IOutgoingIntegration } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Integrations } from '@rocket.chat/models';
|
||||
import { removeEmpty } from '@rocket.chat/tools';
|
||||
import { Match, check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../../lib/deprecationWarningLogger';
|
||||
import { validateOutgoingIntegration } from '../../../lib/integrations/lib/validateOutgoingIntegration';
|
||||
import { validateScriptEngine } from '../../../lib/integrations/lib/validateScriptEngine';
|
||||
import { notifyOnIntegrationChanged } from '../../../lib/notifyListener';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
addOutgoingIntegration(integration: INewOutgoingIntegration): Promise<IOutgoingIntegration>;
|
||||
}
|
||||
}
|
||||
|
||||
export const addOutgoingIntegration = async (userId: string, integration: INewOutgoingIntegration): Promise<IOutgoingIntegration> => {
|
||||
check(
|
||||
integration,
|
||||
Match.ObjectIncluding({
|
||||
type: String,
|
||||
name: String,
|
||||
enabled: Boolean,
|
||||
username: String,
|
||||
channel: String,
|
||||
alias: Match.Maybe(String),
|
||||
emoji: Match.Maybe(String),
|
||||
scriptEnabled: Boolean,
|
||||
script: Match.Maybe(String),
|
||||
scriptEngine: Match.Maybe(String),
|
||||
urls: Match.Maybe([String]),
|
||||
event: Match.Maybe(String),
|
||||
triggerWords: Match.Maybe([String]),
|
||||
avatar: Match.Maybe(String),
|
||||
token: Match.Maybe(String),
|
||||
impersonateUser: Match.Maybe(Boolean),
|
||||
retryCount: Match.Maybe(Number),
|
||||
retryDelay: Match.Maybe(String),
|
||||
retryFailedCalls: Match.Maybe(Boolean),
|
||||
runOnEdits: Match.Maybe(Boolean),
|
||||
targetRoom: Match.Maybe(String),
|
||||
triggerWordAnywhere: Match.Maybe(Boolean),
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
!userId ||
|
||||
(!(await hasPermissionAsync(userId, 'manage-outgoing-integrations')) &&
|
||||
!(await hasPermissionAsync(userId, 'manage-own-outgoing-integrations')))
|
||||
) {
|
||||
throw new Meteor.Error('not_authorized');
|
||||
}
|
||||
|
||||
if (integration.script?.trim()) {
|
||||
validateScriptEngine(integration.scriptEngine ?? 'isolated-vm');
|
||||
}
|
||||
|
||||
const integrationData = await validateOutgoingIntegration(integration, userId);
|
||||
|
||||
const { insertedId } = await Integrations.insertOne(removeEmpty(integrationData));
|
||||
|
||||
const integrationStored = await Integrations.findOne({ _id: insertedId });
|
||||
|
||||
if (!integrationStored) {
|
||||
throw new Error('Error inserting integration');
|
||||
}
|
||||
|
||||
void notifyOnIntegrationChanged({ ...integrationStored, _id: insertedId }, 'inserted');
|
||||
|
||||
return integrationStored as IOutgoingIntegration;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async addOutgoingIntegration(integration: INewOutgoingIntegration): Promise<IOutgoingIntegration> {
|
||||
methodDeprecationLogger.method('addOutgoingIntegration', '9.0.0', '/v1/integrations.create');
|
||||
const { userId } = this;
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('Invalid User');
|
||||
}
|
||||
|
||||
return addOutgoingIntegration(userId, integration);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Integrations, IntegrationHistory } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../../lib/deprecationWarningLogger';
|
||||
import { notifyOnIntegrationChanged } from '../../../lib/notifyListener';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
deleteOutgoingIntegration(integrationId: string): Promise<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteOutgoingIntegration = async (integrationId: string, userId: string): Promise<void> => {
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('not_authorized', 'Unauthorized', {
|
||||
method: 'deleteOutgoingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
const canManageAllIntegrations = await hasPermissionAsync(userId, 'manage-outgoing-integrations');
|
||||
const canManageOwnIntegrations = !canManageAllIntegrations && (await hasPermissionAsync(userId, 'manage-own-outgoing-integrations'));
|
||||
|
||||
if (!canManageAllIntegrations && !canManageOwnIntegrations) {
|
||||
throw new Meteor.Error('not_authorized', 'Unauthorized', {
|
||||
method: 'deleteOutgoingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
const integration = await Integrations.removeByIdAndCreatedByIfExists({
|
||||
_id: integrationId,
|
||||
...(canManageOwnIntegrations && { createdBy: userId }),
|
||||
});
|
||||
|
||||
if (!integration) {
|
||||
throw new Meteor.Error('error-invalid-integration', 'Invalid integration', {
|
||||
method: 'deleteOutgoingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
// Don't sending to IntegrationHistory listener since it don't waits for 'removed' events.
|
||||
await IntegrationHistory.removeByIntegrationId(integrationId);
|
||||
void notifyOnIntegrationChanged(integration, 'removed');
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async deleteOutgoingIntegration(integrationId) {
|
||||
methodDeprecationLogger.method('deleteOutgoingIntegration', '9.0.0', '/v1/integrations.remove');
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('not_authorized', 'Unauthorized', {
|
||||
method: 'deleteOutgoingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
await deleteOutgoingIntegration(integrationId, userId);
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../../lib/deprecationWarningLogger';
|
||||
import { replayOutgoingIntegrationMethod } from '../../../lib/integrations/functions/clearIntegrationHistory';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
replayOutgoingIntegration(params: { integrationId: string; historyId: string }): Promise<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async replayOutgoingIntegration({ integrationId, historyId }) {
|
||||
methodDeprecationLogger.method('replayOutgoingIntegration', '9.0.0', '/v1/integrations.replayOutgoing');
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'replayOutgoingIntegration' });
|
||||
}
|
||||
await replayOutgoingIntegrationMethod(this.userId, { integrationId, historyId });
|
||||
return true;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import type { IIntegration, INewOutgoingIntegration, IUpdateOutgoingIntegration } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Integrations, Users } from '@rocket.chat/models';
|
||||
import { wrapExceptions } from '@rocket.chat/tools';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../../lib/deprecationWarningLogger';
|
||||
import { validateOutgoingIntegration } from '../../../lib/integrations/lib/validateOutgoingIntegration';
|
||||
import { isScriptEngineFrozen, validateScriptEngine } from '../../../lib/integrations/lib/validateScriptEngine';
|
||||
import { notifyOnIntegrationChanged } from '../../../lib/notifyListener';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
updateOutgoingIntegration(
|
||||
integrationId: string,
|
||||
integration: INewOutgoingIntegration | IUpdateOutgoingIntegration,
|
||||
): IIntegration | null;
|
||||
}
|
||||
}
|
||||
|
||||
export const updateOutgoingIntegration = async (
|
||||
userId: string,
|
||||
integrationId: string,
|
||||
_integration: INewOutgoingIntegration | IUpdateOutgoingIntegration,
|
||||
): Promise<IIntegration | null> => {
|
||||
const integration = await validateOutgoingIntegration(_integration, userId);
|
||||
|
||||
if (!integration.token || integration.token.trim() === '') {
|
||||
throw new Meteor.Error('error-invalid-token', 'Invalid token', {
|
||||
method: 'updateOutgoingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
let currentIntegration: IIntegration | null;
|
||||
|
||||
if (await hasPermissionAsync(userId, 'manage-outgoing-integrations')) {
|
||||
currentIntegration = await Integrations.findOneById(integrationId);
|
||||
} else if (await hasPermissionAsync(userId, 'manage-own-outgoing-integrations')) {
|
||||
currentIntegration = await Integrations.findOne({
|
||||
'_id': integrationId,
|
||||
'_createdBy._id': userId,
|
||||
});
|
||||
} else {
|
||||
throw new Meteor.Error('not_authorized', 'Unauthorized', {
|
||||
method: 'updateOutgoingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
if (!currentIntegration) {
|
||||
throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found');
|
||||
}
|
||||
|
||||
const oldScriptEngine = currentIntegration.scriptEngine;
|
||||
const scriptEngine = integration.scriptEngine ?? oldScriptEngine ?? 'isolated-vm';
|
||||
if (
|
||||
integration.script?.trim() &&
|
||||
(scriptEngine !== oldScriptEngine || integration.script?.trim() !== currentIntegration.script?.trim())
|
||||
) {
|
||||
wrapExceptions(() => validateScriptEngine(scriptEngine)).catch((e) => {
|
||||
throw new Meteor.Error(e.message);
|
||||
});
|
||||
}
|
||||
|
||||
const isFrozen = isScriptEngineFrozen(scriptEngine);
|
||||
|
||||
const updatedIntegration = await Integrations.findOneAndUpdate(
|
||||
{ _id: integrationId },
|
||||
{
|
||||
$set: {
|
||||
event: integration.event,
|
||||
enabled: integration.enabled,
|
||||
name: integration.name,
|
||||
avatar: integration.avatar,
|
||||
emoji: integration.emoji,
|
||||
alias: integration.alias,
|
||||
channel: typeof integration.channel === 'string' ? [integration.channel] : integration.channel,
|
||||
targetRoom: integration.targetRoom,
|
||||
impersonateUser: integration.impersonateUser,
|
||||
username: integration.username,
|
||||
userId: integration.userId,
|
||||
urls: integration.urls,
|
||||
token: integration.token,
|
||||
...(isFrozen
|
||||
? {}
|
||||
: {
|
||||
script: integration.script,
|
||||
scriptEnabled: integration.scriptEnabled,
|
||||
scriptEngine,
|
||||
skipTranspile: integration.skipTranspile,
|
||||
...(integration.scriptCompiled ? { scriptCompiled: integration.scriptCompiled } : { scriptError: integration.scriptError }),
|
||||
}),
|
||||
triggerWords: integration.triggerWords,
|
||||
retryFailedCalls: integration.retryFailedCalls,
|
||||
retryCount: integration.retryCount,
|
||||
retryDelay: integration.retryDelay,
|
||||
triggerWordAnywhere: integration.triggerWordAnywhere,
|
||||
runOnEdits: integration.runOnEdits,
|
||||
_updatedAt: new Date(),
|
||||
_updatedBy: await Users.findOne({ _id: userId }, { projection: { username: 1 } }),
|
||||
},
|
||||
...(isFrozen
|
||||
? {}
|
||||
: {
|
||||
$unset: {
|
||||
...(integration.scriptCompiled ? { scriptError: 1 as const } : { scriptCompiled: 1 as const }),
|
||||
},
|
||||
}),
|
||||
},
|
||||
{ returnDocument: 'after' },
|
||||
);
|
||||
|
||||
if (updatedIntegration) {
|
||||
await notifyOnIntegrationChanged(updatedIntegration);
|
||||
}
|
||||
|
||||
return updatedIntegration;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async updateOutgoingIntegration(integrationId, _integration) {
|
||||
methodDeprecationLogger.method('updateOutgoingIntegration', '9.0.0', '/v1/integrations.update');
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'updateOutgoingIntegration',
|
||||
});
|
||||
}
|
||||
|
||||
return updateOutgoingIntegration(this.userId, integrationId, _integration);
|
||||
},
|
||||
});
|
||||
27
apps/meteor/server/meteor-methods/media/deleteCustomSound.ts
Normal file
27
apps/meteor/server/meteor-methods/media/deleteCustomSound.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { ICustomSound } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { deleteCustomSound } from '../../lib/media/custom-sounds/lib/deleteCustomSound';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
deleteCustomSound(_id: ICustomSound['_id']): Promise<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async deleteCustomSound(_id) {
|
||||
methodDeprecationLogger.method('deleteCustomSound', '9.0.0', '/v1/custom-sounds.delete');
|
||||
if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) {
|
||||
throw new Meteor.Error('not_authorized');
|
||||
}
|
||||
check(_id, String);
|
||||
await deleteCustomSound(_id);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
46
apps/meteor/server/meteor-methods/media/deleteEmojiCustom.ts
Normal file
46
apps/meteor/server/meteor-methods/media/deleteEmojiCustom.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { api } from '@rocket.chat/core-services';
|
||||
import type { ICustomEmojiDescriptor } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { EmojiCustom } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { RocketChatFileEmojiCustomInstance } from '../../lib/media/emoji-custom/startup/emoji-custom';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
deleteEmojiCustom(emojiID: ICustomEmojiDescriptor['_id']): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const deleteEmojiCustom = async (userId: string, emojiID: ICustomEmojiDescriptor['_id']): Promise<boolean> => {
|
||||
if (!(await hasPermissionAsync(userId, 'manage-emoji'))) {
|
||||
throw new Meteor.Error('not_authorized');
|
||||
}
|
||||
|
||||
const emoji = await EmojiCustom.findOneById(emojiID);
|
||||
if (emoji == null) {
|
||||
throw new Meteor.Error('Custom_Emoji_Error_Invalid_Emoji', 'Invalid emoji', {
|
||||
method: 'deleteEmojiCustom',
|
||||
});
|
||||
}
|
||||
|
||||
await RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${emoji.name}.${emoji.extension}`));
|
||||
await EmojiCustom.removeById(emojiID);
|
||||
void api.broadcast('emoji.deleteCustom', emoji);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async deleteEmojiCustom(emojiID) {
|
||||
methodDeprecationLogger.method('deleteEmojiCustom', '9.0.0', '/v1/emoji-custom.delete');
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error('not_authorized');
|
||||
}
|
||||
|
||||
return deleteEmojiCustom(this.userId, emojiID);
|
||||
},
|
||||
});
|
||||
35
apps/meteor/server/meteor-methods/media/getS3FileUrl.ts
Normal file
35
apps/meteor/server/meteor-methods/media/getS3FileUrl.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Rooms, Uploads } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomAsync } from '../../lib/authorization';
|
||||
import { settings } from '../../settings';
|
||||
import { UploadFS } from '../../ufs';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getS3FileUrl(fileId: string): string;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getS3FileUrl(fileId) {
|
||||
check(fileId, String);
|
||||
const uid = Meteor.userId();
|
||||
if (settings.get<boolean>('FileUpload_ProtectFiles') && !uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendFileMessage' });
|
||||
}
|
||||
const file = await Uploads.findOneById(fileId);
|
||||
if (!file?.rid) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed');
|
||||
}
|
||||
const room = await Rooms.findOneById(file.rid);
|
||||
if (uid && room && !(await canAccessRoomAsync(room, { _id: uid }))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed');
|
||||
}
|
||||
|
||||
return UploadFS.getStore('AmazonS3:Uploads').getRedirectURL(file);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { insertOrUpdateEmoji } from '../../lib/media/emoji-custom/lib/insertOrUpdateEmoji';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
insertOrUpdateEmoji(emojiData: {
|
||||
_id?: string;
|
||||
name: string;
|
||||
aliases: string;
|
||||
extension: string;
|
||||
previousName?: string;
|
||||
previousExtension?: string;
|
||||
newFile?: boolean;
|
||||
}): string | boolean;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async insertOrUpdateEmoji(emojiData) {
|
||||
methodDeprecationLogger.method('insertOrUpdateEmoji', '9.0.0', ['/v1/emoji-custom.create', '/v1/emoji-custom.update']);
|
||||
const emoji = await insertOrUpdateEmoji(this.userId, emojiData);
|
||||
|
||||
if (!emojiData._id) {
|
||||
return emoji._id;
|
||||
}
|
||||
|
||||
return !!emoji;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { insertOrUpdateSound } from '../../lib/media/custom-sounds/lib/insertOrUpdateSound';
|
||||
|
||||
export type ICustomSoundData = {
|
||||
_id?: string;
|
||||
name: string;
|
||||
extension: string;
|
||||
previousName?: string;
|
||||
previousSound?: {
|
||||
extension?: string;
|
||||
};
|
||||
previousExtension?: string;
|
||||
newFile?: boolean;
|
||||
random?: number;
|
||||
};
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
insertOrUpdateSound(soundData: ICustomSoundData): Promise<string>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async insertOrUpdateSound(soundData) {
|
||||
methodDeprecationLogger.method('insertOrUpdateSound', '9.0.0', ['/v1/custom-sounds.create', '/v1/custom-sounds.update']);
|
||||
if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) {
|
||||
throw new Meteor.Error('not_authorized');
|
||||
}
|
||||
if (soundData._id) {
|
||||
check(soundData._id, String);
|
||||
}
|
||||
return insertOrUpdateSound(soundData);
|
||||
},
|
||||
});
|
||||
20
apps/meteor/server/meteor-methods/media/listCustomSounds.ts
Normal file
20
apps/meteor/server/meteor-methods/media/listCustomSounds.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ICustomSound } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { CustomSounds } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
listCustomSounds(): ICustomSound[];
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async listCustomSounds() {
|
||||
methodDeprecationLogger.method('listCustomSounds', '9.0.0', '/v1/custom-sounds.list');
|
||||
return CustomSounds.find({}).toArray();
|
||||
},
|
||||
});
|
||||
26
apps/meteor/server/meteor-methods/media/uploadCustomSound.ts
Normal file
26
apps/meteor/server/meteor-methods/media/uploadCustomSound.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { RequiredField } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import type { ICustomSoundData } from './insertOrUpdateSound';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { uploadCustomSound } from '../../lib/media/custom-sounds/lib/uploadCustomSound';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
uploadCustomSound(binaryContent: string, contentType: string, soundData: RequiredField<ICustomSoundData, '_id'>): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async uploadCustomSound(binaryContent, contentType, soundData) {
|
||||
methodDeprecationLogger.method('uploadCustomSound', '9.0.0', ['/v1/custom-sounds.create', '/v1/custom-sounds.update']);
|
||||
if (!this.userId || !(await hasPermissionAsync(this.userId, 'manage-sounds'))) {
|
||||
throw new Meteor.Error('not_authorized');
|
||||
}
|
||||
const buffer = Buffer.from(binaryContent, 'binary');
|
||||
await uploadCustomSound(buffer, contentType, soundData);
|
||||
},
|
||||
});
|
||||
27
apps/meteor/server/meteor-methods/media/uploadEmojiCustom.ts
Normal file
27
apps/meteor/server/meteor-methods/media/uploadEmojiCustom.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { uploadEmojiCustom } from '../../lib/media/emoji-custom/lib/uploadEmojiCustom';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
uploadEmojiCustom(
|
||||
binaryContent: string,
|
||||
contentType: string,
|
||||
emojiData: {
|
||||
name: string;
|
||||
aliases?: string;
|
||||
extension: string;
|
||||
},
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async uploadEmojiCustom(binaryContent, contentType, emojiData) {
|
||||
methodDeprecationLogger.method('uploadEmojiCustom', '9.0.0', '/v1/emoji-custom.create');
|
||||
await uploadEmojiCustom(this.userId, binaryContent, contentType, emojiData);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { ICreateRoomParams } from '@rocket.chat/core-services';
|
||||
import type { ICreatedRoom, IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Rooms, Users } from '@rocket.chat/models';
|
||||
import { check, Match } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { RateLimiterClass as RateLimiter } from '../../lib/RateLimiter';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { createRoom } from '../../lib/rooms/createRoom';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export async function createDirectMessage(
|
||||
usernames: IUser['username'][],
|
||||
userId: IUser['_id'] | null,
|
||||
excludeSelf = false,
|
||||
): Promise<Omit<ICreatedRoom, '_id' | 'inserted'>> {
|
||||
check(usernames, [String]);
|
||||
check(userId, String);
|
||||
check(excludeSelf, Match.Optional(Boolean));
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'createDirectMessage',
|
||||
});
|
||||
}
|
||||
|
||||
const me = await Users.findOneById(userId, { projection: { username: 1, name: 1 } });
|
||||
if (!me?.username) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'createDirectMessage',
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.get('Message_AllowDirectMessagesToYourself') === false && usernames.length === 1 && me.username === usernames[0]) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'createDirectMessage',
|
||||
});
|
||||
}
|
||||
|
||||
const users = await Promise.all(usernames.filter((username) => username !== me.username));
|
||||
const options: Exclude<ICreateRoomParams['options'], undefined> = { creator: me._id };
|
||||
const roomUsers = excludeSelf ? users : [me, ...users];
|
||||
|
||||
// allow self-DMs
|
||||
if (roomUsers.length === 1 && roomUsers[0] !== undefined && typeof roomUsers[0] !== 'string' && roomUsers[0]._id !== me._id) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'createDirectMessage',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(userId, 'create-d'))) {
|
||||
// If the user can't create DMs but can access already existing ones
|
||||
if ((await hasPermissionAsync(userId, 'view-d-room')) && !Object.keys(roomUsers).some((user) => typeof user === 'string')) {
|
||||
// Check if the direct room already exists, then return it
|
||||
const uids = (roomUsers as IUser[]).map(({ _id }) => _id).sort();
|
||||
// No projection: the full room is spread into the ICreatedRoom-shaped return below.
|
||||
const room = await Rooms.findOneDirectRoomContainingAllUserIDs(uids);
|
||||
if (room) {
|
||||
return {
|
||||
...room,
|
||||
t: 'd',
|
||||
rid: room._id,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
|
||||
method: 'createDirectMessage',
|
||||
});
|
||||
}
|
||||
|
||||
if (excludeSelf && (await hasPermissionAsync(userId, 'view-room-administration'))) {
|
||||
options.subscriptionExtra = { open: true };
|
||||
}
|
||||
|
||||
const extraData = {};
|
||||
|
||||
try {
|
||||
await callbacks.run('federation.beforeCreateDirectMessage', roomUsers, extraData);
|
||||
} catch (error) {
|
||||
throw new Meteor.Error((error as any)?.message);
|
||||
}
|
||||
const {
|
||||
_id: rid,
|
||||
inserted,
|
||||
...room
|
||||
} = await createRoom<'d'>('d', undefined, undefined, roomUsers as IUser[], false, undefined, extraData, options);
|
||||
|
||||
return {
|
||||
// @ts-expect-error - room type is already defined in the `createRoom` return type
|
||||
t: 'd',
|
||||
// @ts-expect-error - room id is not defined in the `createRoom` return type
|
||||
rid,
|
||||
...room,
|
||||
};
|
||||
}
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
createDirectMessage(...usernames: Exclude<IUser['username'], undefined>[]): Omit<ICreatedRoom, '_id' | 'inserted'>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async createDirectMessage(...usernames) {
|
||||
return createDirectMessage(usernames, Meteor.userId());
|
||||
},
|
||||
});
|
||||
|
||||
RateLimiter.limitMethod('createDirectMessage', 10, 60000, {
|
||||
async userId(userId: IUser['_id']) {
|
||||
return !(await hasPermissionAsync(userId, 'send-many-messages'));
|
||||
},
|
||||
});
|
||||
277
apps/meteor/server/meteor-methods/messages/createDiscussion.ts
Normal file
277
apps/meteor/server/meteor-methods/messages/createDiscussion.ts
Normal file
@@ -0,0 +1,277 @@
|
||||
import { Message } from '@rocket.chat/core-services';
|
||||
import type { IMessage, IRoom, IUser, MessageAttachmentDefault } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages, Rooms, Users } from '@rocket.chat/models';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import { check, Match } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { afterSaveMessageAsync } from '../../hooks/messages/afterSaveMessage';
|
||||
import { canSendMessageAsync } from '../../lib/authorization/canSendMessage';
|
||||
import { hasAtLeastOnePermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { i18n } from '../../lib/i18n';
|
||||
import { attachMessage } from '../../lib/messages/attachMessage';
|
||||
import { sendMessage } from '../../lib/messages/sendMessage';
|
||||
import { addUserToRoom } from '../../lib/rooms/addUserToRoom';
|
||||
import { createRoom } from '../../lib/rooms/createRoom';
|
||||
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
const getParentRoom = async (rid: IRoom['_id']) => {
|
||||
const room = await Rooms.findOne(rid);
|
||||
return room && (room.prid ? Rooms.findOne(room.prid, { projection: { _id: 1 } }) : room);
|
||||
};
|
||||
|
||||
async function createDiscussionMessage(
|
||||
rid: IRoom['_id'],
|
||||
user: IUser,
|
||||
drid: IRoom['_id'],
|
||||
msg: IMessage['msg'],
|
||||
messageEmbedded?: MessageAttachmentDefault,
|
||||
): Promise<IMessage> {
|
||||
return Message.saveSystemMessage('discussion-created', rid, msg, user, {
|
||||
drid,
|
||||
...(messageEmbedded && { attachments: [messageEmbedded] }),
|
||||
});
|
||||
}
|
||||
|
||||
async function mentionMessage(
|
||||
rid: IRoom['_id'],
|
||||
{ _id, username, name }: Pick<IUser, '_id' | 'username' | 'name'>,
|
||||
messageEmbedded?: MessageAttachmentDefault,
|
||||
) {
|
||||
if (!username) {
|
||||
return null;
|
||||
}
|
||||
await Messages.insertOne({
|
||||
rid,
|
||||
msg: '',
|
||||
u: { _id, username, name },
|
||||
ts: new Date(),
|
||||
_updatedAt: new Date(),
|
||||
...(messageEmbedded && { attachments: [messageEmbedded] }),
|
||||
});
|
||||
}
|
||||
|
||||
type CreateDiscussionProperties = {
|
||||
prid: IRoom['_id'];
|
||||
pmid?: IMessage['_id'];
|
||||
t_name: string;
|
||||
reply?: string;
|
||||
users: Array<Exclude<IUser['username'], undefined>>;
|
||||
user: IUser;
|
||||
encrypted?: boolean;
|
||||
topic?: string;
|
||||
};
|
||||
|
||||
const create = async ({
|
||||
prid,
|
||||
pmid,
|
||||
t_name: discussionName,
|
||||
reply,
|
||||
users,
|
||||
user,
|
||||
encrypted,
|
||||
topic,
|
||||
}: CreateDiscussionProperties): Promise<IRoom & { rid: string }> => {
|
||||
// if you set both, prid and pmid, and the rooms dont match... should throw an error)
|
||||
let message: null | IMessage = null;
|
||||
if (pmid) {
|
||||
message = await Messages.findOneById(pmid);
|
||||
if (!message) {
|
||||
throw new Meteor.Error('error-invalid-message', 'Invalid message', {
|
||||
method: 'DiscussionCreation',
|
||||
});
|
||||
}
|
||||
if (prid) {
|
||||
const parentRoom = await getParentRoom(message.rid);
|
||||
if (prid !== parentRoom?._id) {
|
||||
throw new Meteor.Error('error-invalid-arguments', 'Root message room ID does not match parent room ID ', {
|
||||
method: 'DiscussionCreation',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
prid = message.rid;
|
||||
}
|
||||
}
|
||||
|
||||
if (!prid) {
|
||||
throw new Meteor.Error('error-invalid-arguments', 'Missing parent room ID', { method: 'DiscussionCreation' });
|
||||
}
|
||||
|
||||
let parentRoom;
|
||||
try {
|
||||
parentRoom = await canSendMessageAsync(prid, { uid: user._id, username: user.username, type: user.type });
|
||||
} catch (error) {
|
||||
throw new Meteor.Error((error as Error).message);
|
||||
}
|
||||
|
||||
if (parentRoom.prid) {
|
||||
throw new Meteor.Error('error-nested-discussion', 'Cannot create nested discussions', {
|
||||
method: 'DiscussionCreation',
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof encrypted !== 'boolean') {
|
||||
encrypted = Boolean(parentRoom.encrypted);
|
||||
}
|
||||
|
||||
if (encrypted && reply) {
|
||||
throw new Meteor.Error('error-invalid-arguments', 'Encrypted discussions must not receive an initial reply.', {
|
||||
method: 'DiscussionCreation',
|
||||
});
|
||||
}
|
||||
|
||||
if (pmid) {
|
||||
// No projection: the full room is spread into the returned discussion below.
|
||||
const discussionAlreadyExists = await Rooms.findOne({
|
||||
prid,
|
||||
pmid,
|
||||
});
|
||||
if (discussionAlreadyExists) {
|
||||
// do not allow multiple discussions to the same message'\
|
||||
await addUserToRoom(discussionAlreadyExists._id, user);
|
||||
return { ...discussionAlreadyExists, rid: discussionAlreadyExists._id };
|
||||
}
|
||||
}
|
||||
|
||||
const name = Random.id();
|
||||
|
||||
// auto invite the replied message owner
|
||||
const invitedUsers = message ? [message.u.username, ...users] : users;
|
||||
|
||||
const type = await roomCoordinator.getRoomDirectives(parentRoom.t).getDiscussionType(parentRoom);
|
||||
const description = parentRoom.encrypted ? '' : message?.msg;
|
||||
const discussionTopic = topic || parentRoom.name;
|
||||
|
||||
if (!type) {
|
||||
throw new Meteor.Error('error-invalid-type', 'Cannot define discussion room type', {
|
||||
method: 'DiscussionCreation',
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
type === 'p' &&
|
||||
!encrypted &&
|
||||
settings.get<boolean>('E2E_Enable') &&
|
||||
settings.get<boolean>('E2E_Force_Encryption_For_Private_Rooms')
|
||||
) {
|
||||
throw new Meteor.Error(
|
||||
'error-encrypted-private-rooms-enforced-discussion',
|
||||
'Workspace policy requires all private rooms to be encrypted. To create this discussion, make the parent channel public or enable encryption on it.',
|
||||
{ method: 'DiscussionCreation' },
|
||||
);
|
||||
}
|
||||
|
||||
const discussion = await createRoom(
|
||||
type,
|
||||
name,
|
||||
user,
|
||||
[...new Set(invitedUsers)].filter(Boolean),
|
||||
false,
|
||||
false,
|
||||
{
|
||||
fname: discussionName,
|
||||
description, // TODO discussions remove
|
||||
topic: discussionTopic,
|
||||
prid,
|
||||
encrypted,
|
||||
},
|
||||
{
|
||||
creator: user._id,
|
||||
},
|
||||
);
|
||||
|
||||
let discussionMsg;
|
||||
if (message) {
|
||||
if (parentRoom.encrypted) {
|
||||
message.msg = i18n.t('Encrypted_message');
|
||||
}
|
||||
await mentionMessage(discussion._id, user, attachMessage(message, parentRoom));
|
||||
|
||||
discussionMsg = await createDiscussionMessage(message.rid, user, discussion._id, discussionName, attachMessage(message, parentRoom));
|
||||
} else {
|
||||
discussionMsg = await createDiscussionMessage(prid, user, discussion._id, discussionName);
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
await sendMessage(user, { msg: reply }, discussion);
|
||||
}
|
||||
|
||||
if (discussionMsg) {
|
||||
afterSaveMessageAsync(discussionMsg, parentRoom, user);
|
||||
}
|
||||
|
||||
return discussion;
|
||||
};
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
createDiscussion: typeof create;
|
||||
}
|
||||
}
|
||||
|
||||
export const createDiscussion = async (
|
||||
userId: string,
|
||||
{ prid, pmid, t_name: discussionName, reply, users, encrypted, topic }: Omit<CreateDiscussionProperties, 'user'>,
|
||||
): Promise<
|
||||
IRoom & {
|
||||
rid: string;
|
||||
}
|
||||
> => {
|
||||
if (!settings.get('Discussion_enabled')) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' });
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'DiscussionCreation',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasAtLeastOnePermissionAsync(userId, ['start-discussion', 'start-discussion-other-user'], prid))) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'You are not allowed to create a discussion', { method: 'createDiscussion' });
|
||||
}
|
||||
const user = await Users.findOneById(userId, { projection: { services: 0 } });
|
||||
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'createDiscussion',
|
||||
});
|
||||
}
|
||||
|
||||
return create({ prid, pmid, t_name: discussionName, reply, users, user, encrypted, topic });
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
/**
|
||||
* Create discussion by room or message
|
||||
* @constructor
|
||||
* @param {string} prid - Parent Room Id - The room id, optional if you send pmid.
|
||||
* @param {string} pmid - Parent Message Id - Create the discussion by a message, optional.
|
||||
* @param {string} reply - The reply, optional
|
||||
* @param {string} t_name - discussion name
|
||||
* @param {string[]} users - users to be added
|
||||
* @param {boolean} encrypted - if the discussion's e2e encryption should be enabled.
|
||||
*/
|
||||
async createDiscussion({ prid, pmid, t_name: discussionName, reply, users, encrypted }: CreateDiscussionProperties) {
|
||||
methodDeprecationLogger.method('createDiscussion', '9.0.0', '/v1/rooms.createDiscussion');
|
||||
check(prid, Match.Maybe(String));
|
||||
check(pmid, Match.Maybe(String));
|
||||
check(reply, Match.Maybe(String));
|
||||
check(discussionName, String);
|
||||
check(users, [String]);
|
||||
check(encrypted, Match.Maybe(Boolean));
|
||||
|
||||
const uid = Meteor.userId();
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'DiscussionCreation',
|
||||
});
|
||||
}
|
||||
|
||||
return createDiscussion(uid, { prid, pmid, t_name: discussionName, reply, users, encrypted });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Upload } from '@rocket.chat/core-services';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages, Users, Uploads } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import type { DeleteResult } from 'mongodb';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { FileUpload } from '../../lib/media/file-upload';
|
||||
import { deleteMessageValidatingPermission } from '../../lib/messages/deleteMessage';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
deleteFileMessage(fileID: string): Promise<void | DeleteResult>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async deleteFileMessage(fileID) {
|
||||
methodDeprecationLogger.method('deleteFileMessage', '9.0.0', '/v1/chat.delete');
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'deleteFileMessage',
|
||||
});
|
||||
}
|
||||
check(fileID, String);
|
||||
|
||||
const msg = await Messages.getMessageByFileId(fileID);
|
||||
|
||||
if (msg) {
|
||||
return deleteMessageValidatingPermission(msg, userId);
|
||||
}
|
||||
|
||||
const user = await Users.findOneById(userId, { projection: { username: 1 } });
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'deleteFileMessage',
|
||||
});
|
||||
}
|
||||
|
||||
const file = await Uploads.findOneById(fileID, { projection: { userId: 1, rid: 1, expiresAt: 1, uploadedAt: 1 } });
|
||||
if (!file) {
|
||||
throw new Meteor.Error('error-invalid-file', 'Invalid file', {
|
||||
method: 'deleteFileMessage',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await Upload.canDeleteFile(user, file, null))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'deleteFileMessage',
|
||||
});
|
||||
}
|
||||
|
||||
return FileUpload.getStore('Uploads').deleteById(fileID);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { IMessage, RequiredField, SlashCommandPreviewItem } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { slashCommands } from '../../lib/utils/slashCommand';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
executeSlashCommandPreview(
|
||||
command: {
|
||||
cmd: string;
|
||||
params: string;
|
||||
msg: RequiredField<Partial<IMessage>, 'rid'>;
|
||||
triggerId?: string;
|
||||
},
|
||||
preview: SlashCommandPreviewItem,
|
||||
): void;
|
||||
}
|
||||
}
|
||||
|
||||
export const executeSlashCommandPreview = async (
|
||||
command: {
|
||||
cmd: string;
|
||||
params: string;
|
||||
msg: RequiredField<Partial<IMessage>, 'rid'>;
|
||||
triggerId?: string;
|
||||
},
|
||||
preview: SlashCommandPreviewItem,
|
||||
userId: string,
|
||||
): Promise<void> => {
|
||||
if (!command?.cmd || !slashCommands.commands[command.cmd]) {
|
||||
throw new Meteor.Error('error-invalid-command', 'Invalid Command Provided', {
|
||||
method: 'executeSlashCommandPreview',
|
||||
});
|
||||
}
|
||||
|
||||
const theCmd = slashCommands.commands[command.cmd];
|
||||
if (!theCmd.providesPreview) {
|
||||
throw new Meteor.Error('error-invalid-command', 'Command Does Not Provide Previews', {
|
||||
method: 'executeSlashCommandPreview',
|
||||
});
|
||||
}
|
||||
|
||||
if (!preview) {
|
||||
throw new Meteor.Error('error-invalid-command-preview', 'Invalid Preview Provided', {
|
||||
method: 'executeSlashCommandPreview',
|
||||
});
|
||||
}
|
||||
|
||||
return slashCommands.executePreview(command.cmd, command.params, command.msg, preview, userId, command.triggerId);
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
executeSlashCommandPreview(command, preview) {
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'getSlashCommandPreview',
|
||||
});
|
||||
}
|
||||
|
||||
return executeSlashCommandPreview(command, preview, userId);
|
||||
},
|
||||
});
|
||||
70
apps/meteor/server/meteor-methods/messages/followMessage.ts
Normal file
70
apps/meteor/server/meteor-methods/messages/followMessage.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Apps, AppEvents } from '@rocket.chat/apps';
|
||||
import type { IMessage, IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { RateLimiterClass as RateLimiter } from '../../lib/RateLimiter';
|
||||
import { canAccessRoomIdAsync } from '../../lib/authorization/canAccessRoom';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { follow } from '../../lib/messaging/threads/functions';
|
||||
import { notifyOnMessageChange } from '../../lib/notifyListener';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
followMessage(message: { mid: IMessage['_id'] }): false | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const followMessage = async (user: IUser, { mid }: { mid: IMessage['_id'] }): Promise<false | undefined> => {
|
||||
if (mid && !settings.get('Threads_enabled')) {
|
||||
throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' });
|
||||
}
|
||||
|
||||
const message = await Messages.findOneById(mid);
|
||||
if (!message) {
|
||||
throw new Meteor.Error('error-invalid-message', 'Invalid message', {
|
||||
method: 'followMessage',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await canAccessRoomIdAsync(message.rid, user._id))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' });
|
||||
}
|
||||
|
||||
const id = message.tmid || message._id;
|
||||
|
||||
const followResult = await follow({ tmid: id, uid: user._id });
|
||||
|
||||
void notifyOnMessageChange({
|
||||
id,
|
||||
});
|
||||
|
||||
const isFollowed = true;
|
||||
await Apps.self?.triggerEvent(AppEvents.IPostMessageFollowed, message, user, isFollowed);
|
||||
|
||||
return followResult;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async followMessage({ mid }) {
|
||||
methodDeprecationLogger.method('followMessage', '9.0.0', '/v1/chat.followMessage');
|
||||
check(mid, String);
|
||||
|
||||
const user = (await Meteor.userAsync()) as IUser;
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'followMessage' });
|
||||
}
|
||||
|
||||
return followMessage(user, { mid });
|
||||
},
|
||||
});
|
||||
|
||||
RateLimiter.limitMethod('followMessage', 5, 5000, {
|
||||
userId() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
173
apps/meteor/server/meteor-methods/messages/getChannelHistory.ts
Normal file
173
apps/meteor/server/meteor-methods/messages/getChannelHistory.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import { Authorization } from '@rocket.chat/core-services';
|
||||
import type { IMessage, MessageTypesValues } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages, Rooms } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { getHiddenSystemMessages } from '../../lib/messaging/getHiddenSystemMessages';
|
||||
import { normalizeMessagesForUser } from '../../lib/utils/lib/normalizeMessagesForUser';
|
||||
import { settings } from '../../settings/cached';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getChannelHistory(params: {
|
||||
rid: string;
|
||||
latest?: Date;
|
||||
oldest?: Date;
|
||||
inclusive?: boolean;
|
||||
offset?: number;
|
||||
count?: number;
|
||||
unreads?: boolean;
|
||||
showThreadMessages?: boolean;
|
||||
}): boolean | IMessage[] | { messages: IMessage[]; firstUnread?: any; unreadNotLoaded?: number };
|
||||
}
|
||||
}
|
||||
|
||||
export const getChannelHistory = async ({
|
||||
rid,
|
||||
fromUserId,
|
||||
latest,
|
||||
oldest,
|
||||
inclusive,
|
||||
offset = 0,
|
||||
count = 20,
|
||||
unreads,
|
||||
showThreadMessages = true,
|
||||
}: {
|
||||
rid: string;
|
||||
fromUserId: string;
|
||||
latest?: Date;
|
||||
oldest?: Date;
|
||||
inclusive?: boolean;
|
||||
offset?: number;
|
||||
count?: number;
|
||||
unreads?: boolean;
|
||||
showThreadMessages?: boolean;
|
||||
}): Promise<false | IMessage[] | { messages: IMessage[]; firstUnread?: any; unreadNotLoaded?: number }> => {
|
||||
check(rid, String);
|
||||
|
||||
if (!fromUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneById(rid);
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure they can access the room
|
||||
if (!(await Authorization.canReadRoom(room, { _id: fromUserId }))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure latest is always defined.
|
||||
if (latest === undefined) {
|
||||
latest = new Date();
|
||||
}
|
||||
|
||||
// Verify oldest is a date if it exists
|
||||
|
||||
if (oldest !== undefined && {}.toString.call(oldest) !== '[object Date]') {
|
||||
throw new Meteor.Error('error-invalid-date', 'Invalid date', { method: 'getChannelHistory' });
|
||||
}
|
||||
|
||||
const hiddenSystemMessages = settings.get<MessageTypesValues[]>('Hide_System_Messages');
|
||||
|
||||
const hiddenMessageTypes = getHiddenSystemMessages(room, hiddenSystemMessages);
|
||||
|
||||
const options: Record<string, unknown> = {
|
||||
sort: {
|
||||
ts: -1,
|
||||
},
|
||||
skip: offset,
|
||||
limit: count,
|
||||
};
|
||||
|
||||
const records =
|
||||
oldest === undefined
|
||||
? await Messages.findVisibleByRoomIdBeforeTimestampNotContainingTypes(
|
||||
rid,
|
||||
latest,
|
||||
hiddenMessageTypes,
|
||||
options,
|
||||
showThreadMessages,
|
||||
inclusive,
|
||||
).toArray()
|
||||
: await Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes(
|
||||
rid,
|
||||
oldest,
|
||||
latest,
|
||||
hiddenMessageTypes,
|
||||
options,
|
||||
showThreadMessages,
|
||||
inclusive,
|
||||
).toArray();
|
||||
|
||||
const messages = await normalizeMessagesForUser(records, fromUserId);
|
||||
|
||||
if (unreads) {
|
||||
let unreadNotLoaded = 0;
|
||||
let firstUnread = undefined;
|
||||
|
||||
if (oldest !== undefined) {
|
||||
const firstMsg = messages[messages.length - 1];
|
||||
if (firstMsg !== undefined && firstMsg.ts > oldest) {
|
||||
const unreadMessages = Messages.findVisibleByRoomIdBetweenTimestampsNotContainingTypes(
|
||||
rid,
|
||||
oldest,
|
||||
firstMsg.ts,
|
||||
hiddenMessageTypes,
|
||||
{
|
||||
limit: 1,
|
||||
sort: {
|
||||
ts: 1,
|
||||
},
|
||||
},
|
||||
showThreadMessages,
|
||||
);
|
||||
|
||||
const totalCursor = await Messages.countVisibleByRoomIdBetweenTimestampsNotContainingTypes(
|
||||
rid,
|
||||
oldest,
|
||||
firstMsg.ts,
|
||||
hiddenMessageTypes,
|
||||
showThreadMessages,
|
||||
);
|
||||
|
||||
firstUnread = (await unreadMessages.toArray())[0];
|
||||
unreadNotLoaded = totalCursor;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
messages: messages || [],
|
||||
firstUnread,
|
||||
unreadNotLoaded,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
messages: messages || [],
|
||||
};
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getChannelHistory({ rid, latest, oldest, inclusive, offset = 0, count = 20, unreads, showThreadMessages = true }) {
|
||||
methodDeprecationLogger.method('getChannelHistory', '9.0.0', '/v1/channels.history');
|
||||
check(rid, String);
|
||||
|
||||
if (!Meteor.userId()) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getChannelHistory' });
|
||||
}
|
||||
|
||||
const fromUserId = Meteor.userId();
|
||||
if (!fromUserId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getChannelHistory({ rid, fromUserId, latest, oldest, inclusive, offset, count, unreads, showThreadMessages });
|
||||
},
|
||||
});
|
||||
45
apps/meteor/server/meteor-methods/messages/getMessages.ts
Normal file
45
apps/meteor/server/meteor-methods/messages/getMessages.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { IMessage } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomIdsAsync } from '../../lib/authorization/canAccessRoom';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getMessages(messages: IMessage['_id'][]): Promise<IMessage[]>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getMessages(messages) {
|
||||
methodDeprecationLogger.method('getMessages', '9.0.0', '/v1/chat.getMessages');
|
||||
check(messages, [String]);
|
||||
|
||||
const user = await Meteor.userAsync();
|
||||
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getMessages' });
|
||||
}
|
||||
|
||||
const msgs = await Messages.findVisibleByIds(messages).toArray();
|
||||
|
||||
if (!msgs.length) {
|
||||
return msgs;
|
||||
}
|
||||
|
||||
if (
|
||||
!(await canAccessRoomIdsAsync(
|
||||
msgs.map((m) => m.rid),
|
||||
user,
|
||||
))
|
||||
) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getMessages' });
|
||||
}
|
||||
|
||||
return msgs;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import type { IMessage } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomIdAsync } from '../../lib/authorization/canAccessRoom';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getSingleMessage(mid: IMessage['_id']): Promise<IMessage | null>;
|
||||
}
|
||||
}
|
||||
|
||||
export const getSingleMessage = async (userId: string, mid: IMessage['_id']): Promise<IMessage | null> => {
|
||||
const msg = await Messages.findOneById(mid);
|
||||
|
||||
if (!msg?.rid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!(await canAccessRoomIdAsync(msg.rid, userId))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getSingleMessage' });
|
||||
}
|
||||
|
||||
return msg;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getSingleMessage(mid) {
|
||||
check(mid, String);
|
||||
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getSingleMessage' });
|
||||
}
|
||||
|
||||
return getSingleMessage(uid, mid);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import type { IMessage, RequiredField, SlashCommandPreviews } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { slashCommands } from '../../lib/utils/slashCommand';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getSlashCommandPreviews(command: {
|
||||
cmd: string;
|
||||
params: string;
|
||||
msg: RequiredField<Partial<IMessage>, 'rid'>;
|
||||
}): SlashCommandPreviews | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const getSlashCommandPreviews = async (command: {
|
||||
cmd: string;
|
||||
params: string;
|
||||
msg: RequiredField<Partial<IMessage>, 'rid'>;
|
||||
userId: string;
|
||||
}): Promise<SlashCommandPreviews | undefined> => {
|
||||
if (!command?.cmd || !slashCommands.commands[command.cmd]) {
|
||||
throw new Meteor.Error('error-invalid-command', 'Invalid Command Provided', {
|
||||
method: 'executeSlashCommandPreview',
|
||||
});
|
||||
}
|
||||
|
||||
const theCmd = slashCommands.commands[command.cmd];
|
||||
if (!theCmd.providesPreview) {
|
||||
throw new Meteor.Error('error-invalid-command', 'Command Does Not Provide Previews', {
|
||||
method: 'executeSlashCommandPreview',
|
||||
});
|
||||
}
|
||||
|
||||
return slashCommands.getPreviews(command.cmd, command.params, command.msg, command.userId);
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getSlashCommandPreviews(command) {
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'getSlashCommandPreview',
|
||||
});
|
||||
}
|
||||
|
||||
return getSlashCommandPreviews({ ...command, userId });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,68 @@
|
||||
import type { IMessage, IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages, Rooms } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomAsync } from '../../lib/authorization';
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { readThread } from '../../lib/messaging/threads/functions';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getThreadMessages(params: { tmid: IMessage['_id']; limit?: number; skip?: number }): Promise<IMessage[]>;
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_LIMIT = 100;
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getThreadMessages({ tmid, limit, skip }) {
|
||||
methodDeprecationLogger.method('getThreadMessages', '9.0.0', '/v1/chat.getThreadMessages');
|
||||
|
||||
if ((limit ?? 0) > MAX_LIMIT) {
|
||||
throw new Meteor.Error('error-not-allowed', `max limit: ${MAX_LIMIT}`, {
|
||||
method: 'getThreadMessages',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Meteor.userId() || !settings.get('Threads_enabled')) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Threads Disabled', {
|
||||
method: 'getThreadMessages',
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof tmid !== 'string') {
|
||||
throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'getThreadMessages' });
|
||||
}
|
||||
|
||||
const thread = await Messages.findOneById(tmid);
|
||||
if (!thread) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const user = await Meteor.userAsync();
|
||||
const room = await Rooms.findOneById(thread.rid);
|
||||
|
||||
if (!user || !room || !(await canAccessRoomAsync(room, user))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getThreadMessages' });
|
||||
}
|
||||
|
||||
if (!thread.tcount) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await callbacks.run('beforeReadMessages', thread.rid, user._id);
|
||||
await readThread({ user: user as IUser, room, tmid: thread._id });
|
||||
|
||||
const result = await Messages.findVisibleThreadByThreadId(thread._id, {
|
||||
...(skip && { skip }),
|
||||
...(limit && { limit }),
|
||||
sort: { ts: -1 },
|
||||
}).toArray();
|
||||
|
||||
return [thread, ...result];
|
||||
},
|
||||
});
|
||||
45
apps/meteor/server/meteor-methods/messages/getThreadsList.ts
Normal file
45
apps/meteor/server/meteor-methods/messages/getThreadsList.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { IMessage, IRoom } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages, Rooms } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomAsync } from '../../lib/authorization';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
const MAX_LIMIT = 100;
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getThreadsList(params: { rid: IRoom['_id']; limit?: number; skip?: number }): IMessage[];
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getThreadsList({ rid, limit = 50, skip = 0 }) {
|
||||
methodDeprecationLogger.method('getThreadsList', '9.0.0', '/v1/chat.getThreadsList');
|
||||
if (limit > MAX_LIMIT) {
|
||||
throw new Meteor.Error('error-not-allowed', `max limit: ${MAX_LIMIT}`, {
|
||||
method: 'getThreadsList',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Meteor.userId() || !settings.get('Threads_enabled')) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Threads Disabled', { method: 'getThreadsList' });
|
||||
}
|
||||
|
||||
if (typeof rid !== 'string') {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'getThreadsList' });
|
||||
}
|
||||
|
||||
const user = await Meteor.userAsync();
|
||||
const room = await Rooms.findOneById(rid);
|
||||
|
||||
if (!user || !room || !(await canAccessRoomAsync(room, user))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not Allowed', { method: 'getThreadsList' });
|
||||
}
|
||||
|
||||
return Messages.findThreadsByRoomId(room._id, skip, limit).toArray();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { IMessage } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages, Users, Rooms } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomAsync } from '../../lib/authorization';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getUserMentionsByChannel(params: { roomId: string; options: { limit: number; skip: number; sort: { ts: -1 | 1 } } }): IMessage[];
|
||||
}
|
||||
}
|
||||
|
||||
export const getUserMentionsByChannel = async (
|
||||
userId: string,
|
||||
roomId: string,
|
||||
options: { limit?: number; skip?: number; sort?: { ts?: -1 | 1 } },
|
||||
) => {
|
||||
check(roomId, String);
|
||||
|
||||
const user = await Users.findOneById(userId);
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user');
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneById(roomId);
|
||||
|
||||
if (!room || !(await canAccessRoomAsync(room, user))) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', {
|
||||
method: 'getUserMentionsByChannel',
|
||||
});
|
||||
}
|
||||
|
||||
return Messages.findVisibleByMentionAndRoomId(user.username, roomId, options).toArray();
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getUserMentionsByChannel({ roomId, options }) {
|
||||
methodDeprecationLogger.method('getUserMentionsByChannel', '9.0.0', '/v1/channels.getAllUserMentionsByChannel');
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'getUserMentionsByChannel',
|
||||
});
|
||||
}
|
||||
|
||||
return getUserMentionsByChannel(uid, roomId, options);
|
||||
},
|
||||
});
|
||||
67
apps/meteor/server/meteor-methods/messages/loadHistory.ts
Normal file
67
apps/meteor/server/meteor-methods/messages/loadHistory.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { IMessage, IRoom } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Subscriptions, Rooms } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomAsync, roomAccessAttributes } from '../../lib/authorization';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { loadMessageHistory } from '../../lib/messages/loadMessageHistory';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
loadHistory(
|
||||
rid: IRoom['_id'],
|
||||
ts?: Date,
|
||||
limit?: number,
|
||||
ls?: string | Date,
|
||||
showThreadMessages?: boolean,
|
||||
):
|
||||
| {
|
||||
messages: IMessage[];
|
||||
firstUnread: IMessage | undefined;
|
||||
unreadNotLoaded: number;
|
||||
}
|
||||
| false;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async loadHistory(rid, end, limit = 20, ls, showThreadMessages = true) {
|
||||
methodDeprecationLogger.method('loadHistory', '9.0.0', '/v1/rooms.history');
|
||||
check(rid, String);
|
||||
const fromUser = await Meteor.userAsync();
|
||||
|
||||
if (!fromUser && settings.get('Accounts_AllowAnonymousRead') === false) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'loadHistory',
|
||||
});
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, t: 1, sysMes: 1 } });
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// this checks the Allow Anonymous Read setting, so no need to check again
|
||||
if (!(await canAccessRoomAsync(room, fromUser || undefined))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if fromId is undefined and it passed the previous check, the user is reading anonymously
|
||||
if (!fromUser) {
|
||||
return loadMessageHistory({ rid, end, limit, ls, showThreadMessages, room });
|
||||
}
|
||||
|
||||
const canPreview = await hasPermissionAsync(fromUser, 'preview-c-room');
|
||||
|
||||
if (room.t === 'c' && !canPreview && !(await Subscriptions.findOneByRoomIdAndUserId(rid, fromUser._id, { projection: { _id: 1 } }))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return loadMessageHistory({ userId: fromUser._id, rid, end, limit, ls, showThreadMessages, room });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { IMessage, IRoom } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomIdAsync } from '../../lib/authorization/canAccessRoom';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
loadMissedMessages(rid: IRoom['_id'], ts: Date): Promise<false | IMessage[]>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async loadMissedMessages(rid, start) {
|
||||
methodDeprecationLogger.method('loadMissedMessages', '9.0.0', '/v1/chat.syncMessages');
|
||||
check(rid, String);
|
||||
check(start, Date);
|
||||
|
||||
const fromId = Meteor.userId() ?? undefined;
|
||||
|
||||
if (!rid) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'getUsersOfRoom' });
|
||||
}
|
||||
|
||||
if (!(await canAccessRoomIdAsync(rid, fromId))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Messages.findVisibleByRoomIdAfterTimestamp(rid, start, true, {
|
||||
sort: {
|
||||
ts: -1,
|
||||
},
|
||||
}).toArray();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { IMessage, IRoom } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomIdAsync } from '../../lib/authorization/canAccessRoom';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { normalizeMessagesForUser } from '../../lib/utils/lib/normalizeMessagesForUser';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
loadNextMessages(rid: IRoom['_id'], end?: Date, limit?: number): Promise<{ messages: IMessage[] }>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async loadNextMessages(rid, end, limit = 20) {
|
||||
methodDeprecationLogger.method('loadNextMessages', '9.0.0', '/v1/rooms.history');
|
||||
check(rid, String);
|
||||
check(limit, Number);
|
||||
|
||||
if (!Meteor.userId()) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'loadNextMessages',
|
||||
});
|
||||
}
|
||||
|
||||
if (!rid) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'loadNextMessages' });
|
||||
}
|
||||
|
||||
const fromId = Meteor.userId();
|
||||
|
||||
if (!fromId || !(await canAccessRoomIdAsync(rid, fromId))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'loadNextMessages' });
|
||||
}
|
||||
|
||||
let records;
|
||||
if (end) {
|
||||
records = await Messages.findVisibleByRoomIdAfterTimestamp(rid, end, true, {
|
||||
sort: {
|
||||
ts: 1,
|
||||
},
|
||||
limit,
|
||||
}).toArray();
|
||||
} else {
|
||||
records = await Messages.findVisibleByRoomId(rid, {
|
||||
sort: {
|
||||
ts: 1,
|
||||
},
|
||||
limit,
|
||||
}).toArray();
|
||||
}
|
||||
|
||||
return {
|
||||
messages: await normalizeMessagesForUser(records, fromId),
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import type { IMessage } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import type { FindOptions } from 'mongodb';
|
||||
|
||||
import { canAccessRoomIdAsync } from '../../lib/authorization/canAccessRoom';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { normalizeMessagesForUser } from '../../lib/utils/lib/normalizeMessagesForUser';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
loadSurroundingMessages(
|
||||
message: Pick<IMessage, '_id' | 'rid'> & { ts?: Date },
|
||||
limit?: number,
|
||||
showThreadMessages?: boolean,
|
||||
):
|
||||
| {
|
||||
messages: IMessage[];
|
||||
moreBefore: boolean;
|
||||
moreAfter: boolean;
|
||||
}
|
||||
| false;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async loadSurroundingMessages(message, limit = 50, showThreadMessages = true) {
|
||||
methodDeprecationLogger.method('loadSurroundingMessages', '9.0.0', '/v1/rooms.history');
|
||||
check(message, Object);
|
||||
check(limit, Number);
|
||||
check(showThreadMessages, Boolean);
|
||||
|
||||
const fromId = Meteor.userId() ?? undefined;
|
||||
|
||||
if (!fromId && settings.get('Accounts_AllowAnonymousRead') === false) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'loadSurroundingMessages',
|
||||
});
|
||||
}
|
||||
|
||||
if (!message._id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const mainMessage = await Messages.findOneById(message._id);
|
||||
|
||||
if (!mainMessage?.rid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(await canAccessRoomIdAsync(mainMessage.rid, fromId))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
limit -= 1;
|
||||
|
||||
const options: FindOptions<IMessage> = {
|
||||
sort: {
|
||||
ts: -1,
|
||||
},
|
||||
limit: Math.ceil(limit / 2),
|
||||
};
|
||||
|
||||
const messages = await Messages.findVisibleByRoomIdBeforeTimestamp(
|
||||
mainMessage.rid,
|
||||
mainMessage.ts,
|
||||
showThreadMessages,
|
||||
options,
|
||||
).toArray();
|
||||
|
||||
const moreBefore = messages.length === options.limit;
|
||||
|
||||
messages.push(mainMessage);
|
||||
|
||||
options.sort = {
|
||||
ts: 1,
|
||||
};
|
||||
|
||||
options.limit = Math.floor(limit / 2);
|
||||
|
||||
const afterMessages = await Messages.findVisibleByRoomIdAfterTimestamp(
|
||||
mainMessage.rid,
|
||||
mainMessage.ts,
|
||||
showThreadMessages,
|
||||
options,
|
||||
).toArray();
|
||||
|
||||
const moreAfter = afterMessages.length === options.limit;
|
||||
|
||||
messages.push(...afterMessages);
|
||||
|
||||
return {
|
||||
messages: fromId ? await normalizeMessagesForUser(messages, fromId) : messages,
|
||||
moreBefore,
|
||||
moreAfter,
|
||||
};
|
||||
},
|
||||
});
|
||||
121
apps/meteor/server/meteor-methods/messages/messageSearch.ts
Normal file
121
apps/meteor/server/meteor-methods/messages/messageSearch.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { ISubscription } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Logger } from '@rocket.chat/logger';
|
||||
import { Messages, Subscriptions, Users } from '@rocket.chat/models';
|
||||
import { Match, check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { readSecondaryPreferred } from '../../database/readSecondaryPreferred';
|
||||
import { canAccessRoomIdAsync } from '../../lib/authorization/canAccessRoom';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { parseMessageSearchQuery } from '../../lib/parseMessageSearchQuery';
|
||||
import type { IRawSearchResult } from '../../lib/search/model/ISearchResult';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
const logger = new Logger('MessageSearch');
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
messageSearch(text: string, rid?: string, limit?: number, offset?: number): IRawSearchResult | false;
|
||||
}
|
||||
}
|
||||
|
||||
export const messageSearch = async function (
|
||||
userId: string,
|
||||
text: string,
|
||||
rid?: string,
|
||||
limit?: number,
|
||||
offset?: number,
|
||||
): Promise<IRawSearchResult | false> {
|
||||
check(text, String);
|
||||
check(rid, Match.Maybe(String));
|
||||
check(limit, Match.Optional(Number));
|
||||
check(offset, Match.Optional(Number));
|
||||
|
||||
// Don't process anything else if the user can't access the room
|
||||
if (rid) {
|
||||
if (!(await canAccessRoomIdAsync(rid, userId))) {
|
||||
return false;
|
||||
}
|
||||
} else if (settings.get('Search.defaultProvider.GlobalSearchEnabled') !== true) {
|
||||
return {
|
||||
message: {
|
||||
docs: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const user = (await Users.findOneById(userId)) || undefined;
|
||||
|
||||
let parsedQuery: ReturnType<typeof parseMessageSearchQuery>;
|
||||
|
||||
try {
|
||||
parsedQuery = parseMessageSearchQuery(text, {
|
||||
user,
|
||||
offset,
|
||||
limit,
|
||||
forceRegex: settings.get('Message_AlwaysSearchRegExp'),
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
logger.error({ msg: 'Error while parsing message search query', error });
|
||||
if (error instanceof SyntaxError) {
|
||||
return { message: { docs: [] } };
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const { query, options } = parsedQuery;
|
||||
|
||||
if (Object.keys(query).length === 0) {
|
||||
return {
|
||||
message: {
|
||||
docs: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
query.t = {
|
||||
$ne: 'rm', // hide removed messages (useful when searching for user messages)
|
||||
};
|
||||
query._hidden = {
|
||||
$ne: true, // don't return _hidden messages
|
||||
};
|
||||
|
||||
if (rid) {
|
||||
query.rid = rid;
|
||||
} else {
|
||||
query.rid = {
|
||||
$in: user?._id ? (await Subscriptions.findByUserId(user._id).toArray()).map((subscription: ISubscription) => subscription.rid) : [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return {
|
||||
message: {
|
||||
docs: await Messages.find(query, {
|
||||
// @ts-expect-error col.s.db is not typed
|
||||
readPreference: readSecondaryPreferred(Messages.col.s.db),
|
||||
...options,
|
||||
}).toArray(),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error({ msg: 'Error while finding messages', error });
|
||||
throw new Error('error-while-finding-messages', { cause: error });
|
||||
}
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async messageSearch(text, rid, limit, offset) {
|
||||
methodDeprecationLogger.method('messageSearch', '9.0.0', '/v1/chat.search');
|
||||
const currentUserId = Meteor.userId();
|
||||
if (!currentUserId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'messageSearch',
|
||||
});
|
||||
}
|
||||
|
||||
return messageSearch(currentUserId, text, rid, limit, offset);
|
||||
},
|
||||
});
|
||||
39
apps/meteor/server/meteor-methods/messages/readMessages.ts
Normal file
39
apps/meteor/server/meteor-methods/messages/readMessages.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Rooms } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomAsync } from '../../lib/authorization';
|
||||
import { readMessages } from '../../lib/readMessages';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
readMessages(rid: string, readThreads?: boolean): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async readMessages(rid, readThreads = false) {
|
||||
check(rid, String);
|
||||
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'readMessages',
|
||||
});
|
||||
}
|
||||
|
||||
const user = ((await Meteor.userAsync()) as IUser | null) ?? undefined;
|
||||
const room = await Rooms.findOneById(rid);
|
||||
if (!room) {
|
||||
throw new Meteor.Error('error-room-does-not-exist', 'This room does not exist', { method: 'readMessages' });
|
||||
}
|
||||
if (!(await canAccessRoomAsync(room, user))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'readMessages' });
|
||||
}
|
||||
|
||||
await readMessages(room, userId, readThreads);
|
||||
},
|
||||
});
|
||||
53
apps/meteor/server/meteor-methods/messages/readThreads.ts
Normal file
53
apps/meteor/server/meteor-methods/messages/readThreads.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { IMessage, IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages, Rooms } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomAsync } from '../../lib/authorization';
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { readThread } from '../../lib/messaging/threads/functions';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
readThreads(tmid: IMessage['_id']): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async readThreads(tmid) {
|
||||
methodDeprecationLogger.method('readThreads', '9.0.0', '/v1/chat.readThread');
|
||||
|
||||
check(tmid, String);
|
||||
|
||||
if (!Meteor.userId() || !settings.get('Threads_enabled')) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Threads Disabled', {
|
||||
method: 'getThreadMessages',
|
||||
});
|
||||
}
|
||||
|
||||
const thread = await Messages.findOneById(tmid);
|
||||
if (!thread) {
|
||||
return;
|
||||
}
|
||||
|
||||
const user = (await Meteor.userAsync()) ?? undefined;
|
||||
|
||||
const room = await Rooms.findOneById(thread.rid);
|
||||
if (!room) {
|
||||
throw new Meteor.Error('error-room-does-not-exist', 'This room does not exist', { method: 'getThreadMessages' });
|
||||
}
|
||||
|
||||
if (!(await canAccessRoomAsync(room, user))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getThreadMessages' });
|
||||
}
|
||||
|
||||
await callbacks.run('beforeReadMessages', thread.rid, user?._id);
|
||||
if (user?._id) {
|
||||
await readThread({ user: user as IUser, room, tmid });
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { expect } from 'chai';
|
||||
import { describe, it, beforeEach } from 'mocha';
|
||||
import proxyquire from 'proxyquire';
|
||||
import sinon from 'sinon';
|
||||
|
||||
const findOneByIdAndUserIdAndRoomId = sinon.stub();
|
||||
const updateFileMetadata = sinon.stub().resolves();
|
||||
const getPath = sinon.stub().returns('/path/to/file.txt');
|
||||
const isImagePreviewSupported = sinon.stub().returns(false);
|
||||
const getFileExtension = sinon.stub().returns('txt');
|
||||
|
||||
const { parseFileIntoMessageAttachments } = proxyquire.noCallThru().load('./sendFileMessage', {
|
||||
'@rocket.chat/models': {
|
||||
Uploads: { findOneByIdAndUserIdAndRoomId, updateFileMetadata },
|
||||
Rooms: { findOneById: sinon.stub() },
|
||||
Users: { findOneById: sinon.stub() },
|
||||
},
|
||||
'meteor/check': {
|
||||
check: sinon.stub(),
|
||||
Match: {
|
||||
Maybe: sinon.stub(),
|
||||
Optional: sinon.stub(),
|
||||
ObjectIncluding: sinon.stub(),
|
||||
},
|
||||
},
|
||||
'meteor/meteor': {
|
||||
Meteor: {
|
||||
Error: class Error extends global.Error {},
|
||||
methods: sinon.stub(),
|
||||
},
|
||||
},
|
||||
'../../lib/media/file-upload/lib/FileUpload': {
|
||||
FileUpload: { getPath },
|
||||
},
|
||||
'../../lib/media/file-upload/isImagePreviewSupported': { isImagePreviewSupported },
|
||||
'../../../lib/utils/getFileExtension': { getFileExtension },
|
||||
'../../lib/callbacks': { callbacks: { runAsync: sinon.stub() } },
|
||||
'../../lib/logger/system': { SystemLogger: { error: sinon.stub() } },
|
||||
'../../lib/authorization/canAccessRoom': { canAccessRoomAsync: sinon.stub().resolves(true) },
|
||||
'./sendMessage': { executeSendMessage: sinon.stub().resolves({}) },
|
||||
});
|
||||
|
||||
describe('sendFileMessage - Mass Assignment & Type Pollution Prevention', () => {
|
||||
const mockUser = { _id: 'user123' };
|
||||
const roomId = 'room123';
|
||||
|
||||
beforeEach(() => {
|
||||
findOneByIdAndUserIdAndRoomId.reset();
|
||||
updateFileMetadata.reset();
|
||||
|
||||
findOneByIdAndUserIdAndRoomId.resolves({ _id: 'file123' });
|
||||
});
|
||||
|
||||
it('should filter out invalid types, nulls, and malicious fields before updating the database', async () => {
|
||||
const maliciousFilePayload = {
|
||||
_id: 'file123',
|
||||
name: null, // invalid type, must be ignored
|
||||
type: 'text/plain',
|
||||
size: 1024,
|
||||
description: 12345, // invalid type, must be ignored
|
||||
typeGroup: 'image', // only valid field
|
||||
content: null, // invalid type, must be ignored
|
||||
maliciousRoleAssignment: 'admin', // mass assignment, must be ignored
|
||||
$set: { bypassSecurity: true }, // mongo injection, must be ignored
|
||||
};
|
||||
|
||||
await parseFileIntoMessageAttachments(maliciousFilePayload as any, roomId, mockUser as any);
|
||||
|
||||
expect(updateFileMetadata.calledOnce).to.equal(true);
|
||||
|
||||
const [fileId, userId, safeMetadata] = updateFileMetadata.getCall(0).args;
|
||||
|
||||
expect(fileId).to.equal('file123');
|
||||
expect(userId).to.equal('user123');
|
||||
|
||||
expect(safeMetadata).to.deep.equal({
|
||||
typeGroup: 'image',
|
||||
});
|
||||
});
|
||||
|
||||
it('should pass valid fields correctly to the database', async () => {
|
||||
const validFilePayload = {
|
||||
_id: 'file123',
|
||||
name: 'picture.jpg',
|
||||
type: 'image/jpeg',
|
||||
size: 2048,
|
||||
description: 'Description',
|
||||
typeGroup: 'image',
|
||||
content: {
|
||||
algorithm: 'rc.v1.aes-sha2',
|
||||
ciphertext: 'test',
|
||||
},
|
||||
};
|
||||
|
||||
await parseFileIntoMessageAttachments(validFilePayload as any, roomId, mockUser as any);
|
||||
|
||||
expect(updateFileMetadata.calledOnce).to.equal(true);
|
||||
|
||||
const [, , safeMetadata] = updateFileMetadata.getCall(0).args;
|
||||
|
||||
expect(safeMetadata).to.deep.equal({
|
||||
name: 'picture.jpg',
|
||||
description: 'Description',
|
||||
typeGroup: 'image',
|
||||
content: {
|
||||
algorithm: 'rc.v1.aes-sha2',
|
||||
ciphertext: 'test',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
255
apps/meteor/server/meteor-methods/messages/sendFileMessage.ts
Normal file
255
apps/meteor/server/meteor-methods/messages/sendFileMessage.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
import type {
|
||||
MessageAttachment,
|
||||
FileAttachmentProps,
|
||||
IUser,
|
||||
IUpload,
|
||||
AtLeast,
|
||||
FilesAndAttachments,
|
||||
IMessage,
|
||||
FileProp,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Rooms, Uploads, Users } from '@rocket.chat/models';
|
||||
import { Match, check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { executeSendMessage } from './sendMessage';
|
||||
import { getFileExtension } from '../../../lib/utils/getFileExtension';
|
||||
import { canAccessRoomAsync } from '../../lib/authorization/canAccessRoom';
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { SystemLogger } from '../../lib/logger/system';
|
||||
import { isImagePreviewSupported } from '../../lib/media/file-upload/isImagePreviewSupported';
|
||||
import { FileUpload } from '../../lib/media/file-upload/lib/FileUpload';
|
||||
|
||||
function validateFileRequiredFields(file: Partial<IUpload>): asserts file is AtLeast<IUpload, '_id' | 'name' | 'type' | 'size'> {
|
||||
const requiredFields = ['_id', 'name', 'type', 'size'];
|
||||
requiredFields.forEach((field) => {
|
||||
if (!Object.keys(file).includes(field)) {
|
||||
throw new Meteor.Error('error-invalid-file', 'Invalid file');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const parseFileIntoMessageAttachments = async (
|
||||
file: Partial<IUpload>,
|
||||
roomId: string,
|
||||
user: IUser,
|
||||
): Promise<FilesAndAttachments> => {
|
||||
validateFileRequiredFields(file);
|
||||
|
||||
const upload = await Uploads.findOneByIdAndUserIdAndRoomId(file._id, user._id, roomId, { projection: { _id: 1 } });
|
||||
if (!upload) {
|
||||
throw new Meteor.Error('error-invalid-file', 'Invalid file', {
|
||||
method: 'sendFileMessage',
|
||||
});
|
||||
}
|
||||
|
||||
const safeMetadata = {
|
||||
...(typeof file.name === 'string' && { name: file.name }),
|
||||
...(typeof file.description === 'string' && { description: file.description }),
|
||||
...(typeof file.typeGroup === 'string' && { typeGroup: file.typeGroup }),
|
||||
...(file.content && typeof file.content === 'object' && { content: file.content }),
|
||||
};
|
||||
|
||||
await Uploads.updateFileMetadata(file._id, user._id, safeMetadata);
|
||||
|
||||
const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`);
|
||||
|
||||
const attachments: MessageAttachment[] = [];
|
||||
|
||||
const files: FileProp[] = [
|
||||
{
|
||||
_id: file._id,
|
||||
name: file.name || '',
|
||||
type: file.type || 'file',
|
||||
size: file.size || 0,
|
||||
format: file.identify?.format || '',
|
||||
typeGroup: file.typeGroup,
|
||||
},
|
||||
];
|
||||
|
||||
if (isImagePreviewSupported(file.type as string)) {
|
||||
const attachment: FileAttachmentProps = {
|
||||
title: file.name,
|
||||
type: 'file',
|
||||
image_alt: file?.description,
|
||||
title_link: fileUrl,
|
||||
title_link_download: true,
|
||||
image_url: fileUrl,
|
||||
image_type: file.type as string,
|
||||
image_size: file.size,
|
||||
fileId: file._id,
|
||||
};
|
||||
|
||||
if (file.identify?.size) {
|
||||
attachment.image_dimensions = file.identify.size;
|
||||
}
|
||||
|
||||
try {
|
||||
attachment.image_preview = await FileUpload.resizeImagePreview(file);
|
||||
const thumbResult = await FileUpload.createImageThumbnail(file);
|
||||
if (thumbResult) {
|
||||
const { data: thumbBuffer, width, height, thumbFileType, thumbFileName, originalFileId } = thumbResult;
|
||||
const thumbnail = await FileUpload.uploadImageThumbnail(
|
||||
{
|
||||
thumbFileName,
|
||||
thumbFileType,
|
||||
originalFileId,
|
||||
},
|
||||
thumbBuffer,
|
||||
roomId,
|
||||
user._id,
|
||||
);
|
||||
const thumbUrl = FileUpload.getPath(`${thumbnail._id}/${encodeURI(file.name || '')}`);
|
||||
attachment.image_url = thumbUrl;
|
||||
attachment.image_type = thumbnail.type;
|
||||
attachment.image_dimensions = {
|
||||
width,
|
||||
height,
|
||||
};
|
||||
files.push({
|
||||
_id: thumbnail._id,
|
||||
name: thumbnail.name || '',
|
||||
type: thumbnail.type || 'file',
|
||||
size: thumbnail.size || 0,
|
||||
format: thumbnail.identify?.format || '',
|
||||
typeGroup: thumbnail.typeGroup || '',
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
SystemLogger.error({ err });
|
||||
}
|
||||
attachments.push(attachment);
|
||||
} else if (/^audio\/.+/.test(file.type as string)) {
|
||||
const attachment: FileAttachmentProps = {
|
||||
title: file.name,
|
||||
type: 'file',
|
||||
description: file.description,
|
||||
title_link: fileUrl,
|
||||
title_link_download: true,
|
||||
audio_url: fileUrl,
|
||||
audio_type: file.type as string,
|
||||
audio_size: file.size,
|
||||
fileId: file._id,
|
||||
};
|
||||
attachments.push(attachment);
|
||||
} else if (/^video\/.+/.test(file.type as string)) {
|
||||
const attachment: FileAttachmentProps = {
|
||||
title: file.name,
|
||||
type: 'file',
|
||||
description: file.description,
|
||||
title_link: fileUrl,
|
||||
title_link_download: true,
|
||||
video_url: fileUrl,
|
||||
video_type: file.type as string,
|
||||
video_size: file.size as number,
|
||||
fileId: file._id,
|
||||
};
|
||||
attachments.push(attachment);
|
||||
} else {
|
||||
const attachment = {
|
||||
title: file.name,
|
||||
type: 'file',
|
||||
format: getFileExtension(file.name),
|
||||
description: file.description,
|
||||
title_link: fileUrl,
|
||||
title_link_download: true,
|
||||
size: file.size as number,
|
||||
fileId: file._id,
|
||||
};
|
||||
attachments.push(attachment);
|
||||
}
|
||||
return { files, attachments };
|
||||
};
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
sendFileMessage: (roomId: string, _store: string, file: Partial<IUpload>, msgData?: Record<string, any>) => boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const sendFileMessage = async (
|
||||
userId: string,
|
||||
{
|
||||
roomId,
|
||||
file,
|
||||
msgData,
|
||||
}: {
|
||||
roomId: string;
|
||||
file: Partial<IUpload>;
|
||||
msgData?: Record<string, any>;
|
||||
},
|
||||
): Promise<boolean> => {
|
||||
const user = await Users.findOneById(userId, { projection: { services: 0 } });
|
||||
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'sendFileMessage',
|
||||
} as any);
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneById(roomId);
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user?.type !== 'app' && !(await canAccessRoomAsync(room, user))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
check(
|
||||
msgData,
|
||||
Match.Maybe({
|
||||
avatar: Match.Optional(String),
|
||||
emoji: Match.Optional(String),
|
||||
alias: Match.Optional(String),
|
||||
groupable: Match.Optional(Boolean),
|
||||
msg: Match.Optional(String),
|
||||
tmid: Match.Optional(String),
|
||||
customFields: Match.Optional(String),
|
||||
t: Match.Optional(String),
|
||||
content: Match.Optional(
|
||||
Match.ObjectIncluding({
|
||||
algorithm: String,
|
||||
ciphertext: String,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
const data = {
|
||||
rid: roomId,
|
||||
ts: new Date(),
|
||||
...(msgData as Partial<IMessage>),
|
||||
...(msgData?.customFields && { customFields: JSON.parse(msgData.customFields) }),
|
||||
msg: msgData?.msg ?? '',
|
||||
groupable: msgData?.groupable ?? false,
|
||||
};
|
||||
|
||||
const { files, attachments } = await parseFileIntoMessageAttachments(file, roomId, user);
|
||||
data.file = files[0];
|
||||
data.files = files;
|
||||
data.attachments = attachments;
|
||||
|
||||
const msg = await executeSendMessage(userId, data);
|
||||
|
||||
callbacks.runAsync('afterFileUpload', { user, room, message: msg });
|
||||
|
||||
return msg;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async sendFileMessage(roomId, _store, file, msgData = {}) {
|
||||
methodDeprecationLogger.method('sendFileMessage', '9.0.0', '/v1/rooms.mediaConfirm/:rid/:fileId');
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'sendFileMessage',
|
||||
} as any);
|
||||
}
|
||||
|
||||
return sendFileMessage(userId, { roomId, file, msgData });
|
||||
},
|
||||
});
|
||||
187
apps/meteor/server/meteor-methods/messages/sendMessage.ts
Normal file
187
apps/meteor/server/meteor-methods/messages/sendMessage.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { api } from '@rocket.chat/core-services';
|
||||
import type { AtLeast, IMessage, IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import type { RocketchatI18nKeys } from '@rocket.chat/i18n';
|
||||
import { MessageTypes } from '@rocket.chat/message-types';
|
||||
import { Messages, Users } from '@rocket.chat/models';
|
||||
import type { TOptions } from 'i18next';
|
||||
import { check, Match } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import moment from 'moment';
|
||||
|
||||
import { RateLimiterClass as RateLimiter } from '../../lib/RateLimiter';
|
||||
import { canSendMessageAsync } from '../../lib/authorization/canSendMessage';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { applyAirGappedRestrictionsValidation } from '../../lib/cloud/license/airGappedRestrictionsWrapper';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { i18n } from '../../lib/i18n';
|
||||
import { SystemLogger } from '../../lib/logger/system';
|
||||
import { sendMessage } from '../../lib/messages/sendMessage';
|
||||
import { metrics } from '../../lib/metrics';
|
||||
import { settings } from '../../settings';
|
||||
/**
|
||||
*
|
||||
* @param uid
|
||||
* @param message
|
||||
* @param extraInfo
|
||||
* - ts: The timestamp of the message. the message object already has a ts, but this value is validated and only a window of 10 seconds is allowed to be used. this value overrides the message.ts value without validation.
|
||||
*
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
export async function executeSendMessage(
|
||||
uid: IUser['_id'] | IUser,
|
||||
message: AtLeast<IMessage, 'rid'>,
|
||||
extraInfo?: { ts?: Date; previewUrls?: string[] },
|
||||
) {
|
||||
if (message.tshow && !message.tmid) {
|
||||
throw new Meteor.Error('invalid-params', 'tshow provided but missing tmid', {
|
||||
method: 'sendMessage',
|
||||
});
|
||||
}
|
||||
|
||||
if (message.tmid && !settings.get('Threads_enabled')) {
|
||||
throw new Meteor.Error('error-not-allowed', 'not-allowed', {
|
||||
method: 'sendMessage',
|
||||
});
|
||||
}
|
||||
|
||||
const isTimestampFromClient = Boolean(!extraInfo?.ts && message.ts);
|
||||
const now = new Date();
|
||||
message.ts = extraInfo?.ts ?? message.ts ?? now;
|
||||
if (isTimestampFromClient) {
|
||||
const tsDiff = Math.abs(moment(message.ts).diff(Date.now()));
|
||||
if (tsDiff > 60000) {
|
||||
throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', {
|
||||
method: 'sendMessage',
|
||||
message_ts: message.ts,
|
||||
server_ts: new Date().getTime(),
|
||||
});
|
||||
}
|
||||
if (tsDiff > 10000) {
|
||||
message.ts = now;
|
||||
}
|
||||
}
|
||||
|
||||
if (message.msg) {
|
||||
if (message.msg.length > (settings.get<number>('Message_MaxAllowedSize') ?? 0)) {
|
||||
throw new Meteor.Error('error-message-size-exceeded', 'Message size exceeds Message_MaxAllowedSize', {
|
||||
method: 'sendMessage',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const user = typeof uid === 'string' ? await Users.findOneById(uid) : uid;
|
||||
if (!user?.username) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user');
|
||||
}
|
||||
|
||||
let { rid } = message;
|
||||
|
||||
// do not allow nested threads
|
||||
if (message.tmid) {
|
||||
const parentMessage = await Messages.findOneById(message.tmid, { projection: { rid: 1, tmid: 1 } });
|
||||
message.tmid = parentMessage?.tmid || message.tmid;
|
||||
|
||||
if (parentMessage?.rid) {
|
||||
rid = parentMessage?.rid;
|
||||
}
|
||||
}
|
||||
|
||||
if (!rid) {
|
||||
throw new Error("The 'rid' property on the message object is missing.");
|
||||
}
|
||||
|
||||
check(rid, String);
|
||||
|
||||
try {
|
||||
const room = await canSendMessageAsync(rid, user);
|
||||
|
||||
if (room.encrypted && settings.get<boolean>('E2E_Enable') && !settings.get<boolean>('E2E_Allow_Unencrypted_Messages')) {
|
||||
if (message.t !== 'e2e') {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed to send un-encrypted messages in an encrypted room', {
|
||||
method: 'sendMessage',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = await sendMessage(user, message, room, { previewUrls: extraInfo?.previewUrls });
|
||||
|
||||
metrics.messagesSent.inc();
|
||||
metrics.messagesSentTotal.inc();
|
||||
|
||||
return result;
|
||||
} catch (err: any) {
|
||||
SystemLogger.error({ msg: 'Error sending message:', err });
|
||||
|
||||
const errorMessage: RocketchatI18nKeys = typeof err === 'string' ? err : err.error || err.message;
|
||||
const errorContext: TOptions = err.details ?? {};
|
||||
void api.broadcast('notify.ephemeralMessage', user._id, message.rid, {
|
||||
msg: i18n.t(errorMessage, { ...errorContext, lng: user.language }),
|
||||
});
|
||||
|
||||
if (typeof err === 'string') {
|
||||
throw new Error(err);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
sendMessage(message: AtLeast<IMessage, '_id' | 'rid' | 'msg'>, previewUrls?: string[]): any;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async sendMessage(message, previewUrls) {
|
||||
methodDeprecationLogger.method('sendMessage', '9.0.0', '/v1/chat.sendMessage');
|
||||
|
||||
check(message, {
|
||||
_id: Match.Maybe(String),
|
||||
rid: Match.Maybe(String),
|
||||
msg: Match.Maybe(String),
|
||||
tmid: Match.Maybe(String),
|
||||
tshow: Match.Maybe(Boolean),
|
||||
ts: Match.Maybe(Date),
|
||||
t: Match.Maybe(String),
|
||||
bot: Match.Maybe(Object),
|
||||
content: Match.Maybe(Object),
|
||||
e2e: Match.Maybe(String),
|
||||
e2eMentions: Match.Maybe(Object),
|
||||
customFields: Match.Maybe(Object),
|
||||
federation: Match.Maybe(Object),
|
||||
groupable: Match.Maybe(Boolean),
|
||||
sentByEmail: Match.Maybe(Boolean),
|
||||
});
|
||||
|
||||
const user = (await Meteor.userAsync()) as IUser;
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'sendMessage',
|
||||
});
|
||||
}
|
||||
|
||||
if (MessageTypes.isSystemMessage(message)) {
|
||||
throw new Error("Cannot send system messages using 'sendMessage'");
|
||||
}
|
||||
|
||||
try {
|
||||
return await applyAirGappedRestrictionsValidation(() => executeSendMessage(user, message, { previewUrls }));
|
||||
} catch (error: any) {
|
||||
if (['error-not-allowed', 'restricted-workspace'].includes(error.error || error.message)) {
|
||||
throw new Meteor.Error(error.error || error.message, error.reason, {
|
||||
method: 'sendMessage',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
// Limit a user, who does not have the "bot" role, to sending 5 msgs/second
|
||||
RateLimiter.limitMethod('sendMessage', 5, 1000, {
|
||||
async userId(userId: IUser['_id']) {
|
||||
return !(await hasPermissionAsync(userId, 'send-many-messages'));
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { Apps, AppEvents } from '@rocket.chat/apps';
|
||||
import type { IMessage, IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { RateLimiterClass as RateLimiter } from '../../lib/RateLimiter';
|
||||
import { canAccessRoomIdAsync } from '../../lib/authorization/canAccessRoom';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { unfollow } from '../../lib/messaging/threads/functions';
|
||||
import { notifyOnMessageChange } from '../../lib/notifyListener';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
unfollowMessage(message: { mid: IMessage['_id'] }): false | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export const unfollowMessage = async (user: IUser, { mid }: { mid: IMessage['_id'] }): Promise<false | undefined> => {
|
||||
if (mid && !settings.get('Threads_enabled')) {
|
||||
throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'unfollowMessage' });
|
||||
}
|
||||
|
||||
const message = await Messages.findOneById(mid);
|
||||
if (!message) {
|
||||
throw new Meteor.Error('error-invalid-message', 'Invalid message', {
|
||||
method: 'unfollowMessage',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await canAccessRoomIdAsync(message.rid, user._id))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'unfollowMessage' });
|
||||
}
|
||||
|
||||
const id = message.tmid || message._id;
|
||||
|
||||
const unfollowResult = await unfollow({ rid: message.rid, tmid: id, uid: user._id });
|
||||
|
||||
void notifyOnMessageChange({
|
||||
id,
|
||||
});
|
||||
|
||||
const isFollowed = false;
|
||||
await Apps.self?.triggerEvent(AppEvents.IPostMessageFollowed, message, user, isFollowed);
|
||||
|
||||
return unfollowResult;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async unfollowMessage({ mid }) {
|
||||
methodDeprecationLogger.method('unfollowMessage', '9.0.0', '/v1/chat.unfollowMessage');
|
||||
check(mid, String);
|
||||
|
||||
const user = (await Meteor.userAsync()) as IUser;
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'unfollowMessage' });
|
||||
}
|
||||
|
||||
return unfollowMessage(user, { mid });
|
||||
},
|
||||
});
|
||||
|
||||
RateLimiter.limitMethod('unfollowMessage', 5, 5000, {
|
||||
userId() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
121
apps/meteor/server/meteor-methods/messages/updateMessage.ts
Normal file
121
apps/meteor/server/meteor-methods/messages/updateMessage.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { IEditedMessage, IMessage, IUser, AtLeast } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages, Users } from '@rocket.chat/models';
|
||||
import { Match, check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import moment from 'moment';
|
||||
|
||||
import { canSendMessageAsync } from '../../lib/authorization/canSendMessage';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { applyAirGappedRestrictionsValidation } from '../../lib/cloud/license/airGappedRestrictionsWrapper';
|
||||
import { updateMessage } from '../../lib/messages/updateMessage';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
const allowedEditedFields = ['tshow', 'alias', 'attachments', 'avatar', 'emoji', 'msg', 'customFields', 'content', 'e2eMentions'];
|
||||
|
||||
export async function executeUpdateMessage(
|
||||
uid: IUser['_id'],
|
||||
message: AtLeast<IMessage, '_id' | 'rid' | 'msg' | 'customFields'> | AtLeast<IMessage, '_id' | 'rid' | 'content'>,
|
||||
previewUrls?: string[],
|
||||
) {
|
||||
const originalMessage = await Messages.findOneById(message._id);
|
||||
if (!originalMessage?._id) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(message).forEach(([key, value]) => {
|
||||
if (!allowedEditedFields.includes(key) && value !== originalMessage[key as keyof IMessage]) {
|
||||
throw new Meteor.Error('error-invalid-update-key', `Cannot update the message ${key}`, {
|
||||
method: 'updateMessage',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// IF the message has custom fields, always update
|
||||
// Ideally, we'll compare the custom fields to check for change, but since we don't know the shape of
|
||||
// custom fields, as it's user defined, we're gonna update
|
||||
const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg;
|
||||
if (msgText === message.msg && !previewUrls && !message.customFields) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!!message.tmid && originalMessage._id === message.tmid) {
|
||||
throw new Meteor.Error('error-message-same-as-tmid', 'Cannot set tmid the same as the _id', {
|
||||
method: 'updateMessage',
|
||||
});
|
||||
}
|
||||
|
||||
if (!originalMessage.tmid && !!message.tmid) {
|
||||
throw new Meteor.Error('error-message-change-to-thread', 'Cannot update message to a thread', { method: 'updateMessage' });
|
||||
}
|
||||
|
||||
const _hasPermission = await hasPermissionAsync(uid, 'edit-message', message.rid);
|
||||
const editAllowed = settings.get('Message_AllowEditing');
|
||||
const editOwn = originalMessage.u && originalMessage.u._id === uid;
|
||||
|
||||
if (!_hasPermission && (!editAllowed || !editOwn)) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Message editing not allowed', {
|
||||
method: 'updateMessage',
|
||||
action: 'Message_editing',
|
||||
});
|
||||
}
|
||||
|
||||
const blockEditInMinutes = settings.get('Message_AllowEditing_BlockEditInMinutes');
|
||||
const bypassBlockTimeLimit = await hasPermissionAsync(uid, 'bypass-time-limit-edit-and-delete', message.rid);
|
||||
|
||||
if (!bypassBlockTimeLimit && Match.test(blockEditInMinutes, Number) && blockEditInMinutes !== 0) {
|
||||
let currentTsDiff = 0;
|
||||
let msgTs;
|
||||
|
||||
if (originalMessage.ts instanceof Date || Match.test(originalMessage.ts, Number)) {
|
||||
msgTs = moment(originalMessage.ts);
|
||||
}
|
||||
if (msgTs) {
|
||||
currentTsDiff = moment().diff(msgTs, 'minutes');
|
||||
}
|
||||
if (currentTsDiff >= blockEditInMinutes) {
|
||||
throw new Meteor.Error('error-message-editing-blocked', 'Message editing is blocked', {
|
||||
method: 'updateMessage',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const user = await Users.findOneById(uid);
|
||||
if (!user) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateMessage' });
|
||||
}
|
||||
await canSendMessageAsync(message.rid, { uid: user._id, username: user.username ?? undefined, ...user });
|
||||
|
||||
// It is possible to have an empty array as the attachments property, so ensure both things exist
|
||||
if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) {
|
||||
originalMessage.attachments[0].description = message.msg;
|
||||
message.attachments = originalMessage.attachments;
|
||||
message.msg = originalMessage.msg;
|
||||
}
|
||||
|
||||
message.u = originalMessage.u;
|
||||
|
||||
return updateMessage(message, user, originalMessage, previewUrls);
|
||||
}
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
updateMessage(message: IEditedMessage, previewUrls?: string[]): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async updateMessage(message: IEditedMessage, previewUrls?: string[]) {
|
||||
check(message, Match.ObjectIncluding({ _id: String }));
|
||||
check(previewUrls, Match.Maybe([String]));
|
||||
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'updateMessage' });
|
||||
}
|
||||
|
||||
return applyAirGappedRestrictionsValidation(() => executeUpdateMessage(uid, message, previewUrls));
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import type {
|
||||
FileAttachmentProps,
|
||||
ImageAttachmentProps,
|
||||
AudioAttachmentProps,
|
||||
VideoAttachmentProps,
|
||||
IUpload,
|
||||
} from '@rocket.chat/core-typings';
|
||||
import { LivechatVisitors, LivechatRooms } from '@rocket.chat/models';
|
||||
import { Random } from '@rocket.chat/random';
|
||||
import { Match, check } from 'meteor/check';
|
||||
|
||||
import { sendMessageLivechat } from './sendMessageLivechat';
|
||||
import { FileUpload } from '../../lib/media/file-upload';
|
||||
|
||||
interface ISendFileLivechatMessage {
|
||||
roomId: string;
|
||||
visitorToken: string;
|
||||
file: IUpload;
|
||||
msgData?: { avatar?: string; emoji?: string; alias?: string; groupable?: boolean; msg?: string };
|
||||
}
|
||||
|
||||
export const sendFileLivechatMessage = async ({ roomId, visitorToken, file, msgData = {} }: ISendFileLivechatMessage): Promise<boolean> => {
|
||||
const visitor = await LivechatVisitors.getVisitorByToken(visitorToken);
|
||||
|
||||
if (!visitor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const room = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(roomId, visitorToken);
|
||||
|
||||
if (!room) {
|
||||
return false;
|
||||
}
|
||||
|
||||
check(msgData, {
|
||||
avatar: Match.Optional(String),
|
||||
emoji: Match.Optional(String),
|
||||
alias: Match.Optional(String),
|
||||
groupable: Match.Optional(Boolean),
|
||||
msg: Match.Optional(String),
|
||||
});
|
||||
|
||||
const fileUrl = file.name && FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`);
|
||||
|
||||
const attachment: Partial<FileAttachmentProps> = {
|
||||
title: file.name,
|
||||
type: 'file',
|
||||
description: file.description,
|
||||
title_link: fileUrl,
|
||||
title_link_download: true,
|
||||
};
|
||||
|
||||
if (file.type && /^image\/.+/.test(file.type)) {
|
||||
(attachment as ImageAttachmentProps).image_url = fileUrl || '';
|
||||
(attachment as ImageAttachmentProps).image_type = file.type;
|
||||
(attachment as ImageAttachmentProps).image_size = file.size;
|
||||
if (file.identify?.size) {
|
||||
(attachment as ImageAttachmentProps).image_dimensions = file.identify.size;
|
||||
}
|
||||
(attachment as ImageAttachmentProps).image_preview = await FileUpload.resizeImagePreview(file);
|
||||
} else if (file.type && /^audio\/.+/.test(file.type)) {
|
||||
(attachment as AudioAttachmentProps).audio_url = fileUrl || '';
|
||||
(attachment as AudioAttachmentProps).audio_type = file.type;
|
||||
(attachment as AudioAttachmentProps).audio_size = file.size;
|
||||
} else if (file.type && /^video\/.+/.test(file.type)) {
|
||||
(attachment as VideoAttachmentProps).video_url = fileUrl || '';
|
||||
(attachment as VideoAttachmentProps).video_type = file.type;
|
||||
(attachment as VideoAttachmentProps).video_size = file.size || 0;
|
||||
}
|
||||
|
||||
const msg = Object.assign(
|
||||
{
|
||||
_id: Random.id(),
|
||||
rid: roomId,
|
||||
ts: new Date(),
|
||||
msg: '',
|
||||
file: {
|
||||
_id: file._id,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
},
|
||||
files: [
|
||||
{
|
||||
_id: file._id,
|
||||
name: file.name,
|
||||
type: file.type,
|
||||
},
|
||||
],
|
||||
groupable: false,
|
||||
attachments: [attachment],
|
||||
token: visitorToken,
|
||||
},
|
||||
msgData,
|
||||
);
|
||||
|
||||
return sendMessageLivechat({ message: msg });
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { LivechatVisitors } from '@rocket.chat/models';
|
||||
import { Match, check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import type { ILivechatMessage } from '../../lib/omnichannel/localTypes';
|
||||
import { sendMessage } from '../../lib/omnichannel/messages';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
interface ILivechatMessageAgent {
|
||||
agentId: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
interface ISendMessageLivechat {
|
||||
message: ILivechatMessage;
|
||||
agent?: ILivechatMessageAgent;
|
||||
}
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
sendMessageLivechat(message: ILivechatMessage, agent: ILivechatMessageAgent): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const sendMessageLivechat = async ({
|
||||
message: { token, _id, rid, msg, file, files, attachments },
|
||||
agent,
|
||||
}: ISendMessageLivechat): Promise<boolean> => {
|
||||
check(token, String);
|
||||
check(_id, String);
|
||||
check(rid, String);
|
||||
check(msg, String);
|
||||
|
||||
check(
|
||||
agent,
|
||||
Match.Maybe({
|
||||
agentId: String,
|
||||
username: String,
|
||||
}),
|
||||
);
|
||||
|
||||
// No projection: the guest is forwarded into the message-sending pipeline, which expects a full visitor.
|
||||
const guest = await LivechatVisitors.getVisitorByToken(token);
|
||||
|
||||
if (!guest) {
|
||||
throw new Meteor.Error('invalid-token');
|
||||
}
|
||||
|
||||
if (settings.get('Livechat_enable_message_character_limit') && msg.length > parseInt(settings.get('Livechat_message_character_limit'))) {
|
||||
throw new Meteor.Error('message-length-exceeds-character-limit');
|
||||
}
|
||||
|
||||
return sendMessage({
|
||||
guest,
|
||||
message: {
|
||||
_id,
|
||||
rid,
|
||||
msg,
|
||||
token,
|
||||
file,
|
||||
files,
|
||||
attachments,
|
||||
},
|
||||
agent,
|
||||
roomInfo: {
|
||||
source: {
|
||||
type: OmnichannelSourceType.API,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async sendMessageLivechat({ token, _id, rid, msg, file, files, attachments }: ILivechatMessage, agent: ILivechatMessageAgent) {
|
||||
methodDeprecationLogger.method('sendMessageLivechat', '9.0.0', '/v1/livechat/message');
|
||||
return sendMessageLivechat({ message: { token, _id, rid, msg, file, files, attachments }, agent });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { OEmbedCache } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
OEmbedCacheCleanup(): { message: string };
|
||||
}
|
||||
}
|
||||
|
||||
export const executeClearOEmbedCache = async () => {
|
||||
const date = new Date();
|
||||
const expirationDays = settings.get<number>('API_EmbedCacheExpirationDays');
|
||||
date.setDate(date.getDate() - expirationDays);
|
||||
return OEmbedCache.removeBeforeDate(date);
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async OEmbedCacheCleanup() {
|
||||
const uid = Meteor.userId();
|
||||
if (!uid || !(await hasPermissionAsync(uid, 'clear-oembed-cache'))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
|
||||
method: 'OEmbedCacheCleanup',
|
||||
});
|
||||
}
|
||||
|
||||
await executeClearOEmbedCache();
|
||||
return {
|
||||
message: 'cache_cleared',
|
||||
};
|
||||
},
|
||||
});
|
||||
33
apps/meteor/server/meteor-methods/platform/banner_dismiss.ts
Normal file
33
apps/meteor/server/meteor-methods/platform/banner_dismiss.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { notifyOnUserChange } from '../../lib/notifyListener';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'banner/dismiss'({ id }: { id: string }): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'banner/dismiss'({ id }) {
|
||||
methodDeprecationLogger.method('banner/dismiss', '9.0.0', '/v1/banners.dismiss');
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'banner/dismiss' });
|
||||
}
|
||||
|
||||
await Users.setBannerReadById(userId, id);
|
||||
|
||||
void notifyOnUserChange({
|
||||
id: userId,
|
||||
clientAction: 'updated',
|
||||
diff: {
|
||||
[`banners.${id}.read`]: true,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { Federation, FederationEE, Authorization } from '@rocket.chat/core-services';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { License } from '@rocket.chat/license';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
checkFederationConfiguration(): Promise<{ message: string }>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async checkFederationConfiguration() {
|
||||
methodDeprecationLogger.method('checkFederationConfiguration', '9.0.0', []);
|
||||
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'checkFederationConfiguration',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await Authorization.hasPermission(uid, 'view-privileged-setting'))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Action not allowed', {
|
||||
method: 'checkFederationConfiguration',
|
||||
});
|
||||
}
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
const successes: string[] = [];
|
||||
|
||||
const service = License.hasValidLicense() ? FederationEE : Federation;
|
||||
|
||||
const status = await service.configurationStatus();
|
||||
|
||||
if (status.externalReachability.ok) {
|
||||
successes.push('homeserver configuration looks good');
|
||||
} else {
|
||||
let err = 'external reachability could not be verified';
|
||||
|
||||
const { error } = status.externalReachability;
|
||||
if (error) {
|
||||
err += `, error: ${error}`;
|
||||
}
|
||||
|
||||
errors.push(err);
|
||||
}
|
||||
|
||||
const {
|
||||
roundTrip: { durationMs: duration },
|
||||
} = status.appservice;
|
||||
|
||||
if (status.appservice.ok) {
|
||||
successes.push(`appservice configuration looks good, total round trip time to homeserver ${duration}ms`);
|
||||
} else {
|
||||
errors.push(`failed to verify appservice configuration: ${status.appservice.error}`);
|
||||
}
|
||||
|
||||
if (errors.length) {
|
||||
void service.markConfigurationInvalid();
|
||||
|
||||
if (successes.length) {
|
||||
const message = ['Configuration could only be partially verified'].concat(successes).concat(errors).join(', ');
|
||||
|
||||
throw new Meteor.Error('error-invalid-configuration', message, { method: 'checkFederationConfiguration' });
|
||||
}
|
||||
|
||||
throw new Meteor.Error('error-invalid-configuration', ['Invalid configuration'].concat(errors).join(', '), {
|
||||
method: 'checkFederationConfiguration',
|
||||
});
|
||||
}
|
||||
|
||||
void service.markConfigurationValid();
|
||||
|
||||
return {
|
||||
message: ['All configuration looks good'].concat(successes).join(', '),
|
||||
};
|
||||
},
|
||||
});
|
||||
214
apps/meteor/server/meteor-methods/platform/cloud.ts
Normal file
214
apps/meteor/server/meteor-methods/platform/cloud.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { buildWorkspaceRegistrationData } from '../../lib/cloud/buildRegistrationData';
|
||||
import { checkUserHasCloudLogin } from '../../lib/cloud/checkUserHasCloudLogin';
|
||||
import { connectWorkspace } from '../../lib/cloud/connectWorkspace';
|
||||
import { finishOAuthAuthorization } from '../../lib/cloud/finishOAuthAuthorization';
|
||||
import { getOAuthAuthorizationUrl } from '../../lib/cloud/getOAuthAuthorizationUrl';
|
||||
import { retrieveRegistrationStatus } from '../../lib/cloud/retrieveRegistrationStatus';
|
||||
import { startRegisterWorkspace } from '../../lib/cloud/startRegisterWorkspace';
|
||||
import { syncWorkspace } from '../../lib/cloud/syncWorkspace';
|
||||
import { userLogout } from '../../lib/cloud/userLogout';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'cloud:checkRegisterStatus': () => {
|
||||
workspaceRegistered: boolean;
|
||||
workspaceId: string;
|
||||
uniqueId: string;
|
||||
token: string;
|
||||
email: string;
|
||||
};
|
||||
'cloud:getWorkspaceRegisterData': () => string;
|
||||
'cloud:registerWorkspace': () => boolean;
|
||||
'cloud:syncWorkspace': () => boolean;
|
||||
'cloud:connectWorkspace': (token: string) => boolean | Error;
|
||||
'cloud:getOAuthAuthorizationUrl': () => string;
|
||||
'cloud:finishOAuthAuthorization': (code: string, state: string) => boolean;
|
||||
'cloud:checkUserLoggedIn': () => boolean;
|
||||
'cloud:logout': () => Promise<boolean | string>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
/**
|
||||
* @deprecated this method is deprecated and will be removed soon.
|
||||
* Prefer using cloud.registrationStatus rest api.
|
||||
*/
|
||||
async 'cloud:checkRegisterStatus'() {
|
||||
methodDeprecationLogger.method('cloud:checkRegisterStatus', '9.0.0', '/v1/cloud.registrationStatus');
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'cloud:checkRegisterStatus',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(uid, 'manage-cloud'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'cloud:checkRegisterStatus',
|
||||
});
|
||||
}
|
||||
|
||||
return retrieveRegistrationStatus();
|
||||
},
|
||||
/**
|
||||
* @deprecated this method is deprecated and will be removed soon.
|
||||
* Prefer using cloud.workspaceRegisterData rest api.
|
||||
*/
|
||||
async 'cloud:getWorkspaceRegisterData'() {
|
||||
methodDeprecationLogger.method('cloud:getWorkspaceRegisterData', '9.0.0', '/v1/cloud.workspaceRegisterData');
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'cloud:getWorkspaceRegisterData',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(uid, 'manage-cloud'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'cloud:getWorkspaceRegisterData',
|
||||
});
|
||||
}
|
||||
|
||||
return Buffer.from(JSON.stringify(await buildWorkspaceRegistrationData(undefined))).toString('base64');
|
||||
},
|
||||
async 'cloud:registerWorkspace'() {
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'cloud:registerWorkspace',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(uid, 'manage-cloud'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'cloud:registerWorkspace',
|
||||
});
|
||||
}
|
||||
|
||||
return startRegisterWorkspace();
|
||||
},
|
||||
async 'cloud:syncWorkspace'() {
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'cloud:syncWorkspace',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(uid, 'manage-cloud'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'cloud:syncWorkspace',
|
||||
});
|
||||
}
|
||||
|
||||
await syncWorkspace();
|
||||
|
||||
return true;
|
||||
},
|
||||
async 'cloud:connectWorkspace'(token) {
|
||||
methodDeprecationLogger.method('cloud:connectWorkspace', '9.0.0', '/v1/cloud.connectWorkspace');
|
||||
check(token, String);
|
||||
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'cloud:connectWorkspace',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(uid, 'manage-cloud'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'cloud:connectWorkspace',
|
||||
});
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new Meteor.Error('error-invalid-payload', 'Token is required.', {
|
||||
method: 'cloud:connectWorkspace',
|
||||
});
|
||||
}
|
||||
|
||||
return connectWorkspace(token);
|
||||
},
|
||||
// Currently unused but will link local account to Rocket.Chat Cloud account.
|
||||
async 'cloud:getOAuthAuthorizationUrl'() {
|
||||
const uid = Meteor.userId();
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'cloud:getOAuthAuthorizationUrl',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(uid, 'manage-cloud'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'cloud:getOAuthAuthorizationUrl',
|
||||
});
|
||||
}
|
||||
|
||||
return getOAuthAuthorizationUrl();
|
||||
},
|
||||
async 'cloud:finishOAuthAuthorization'(code, state) {
|
||||
check(code, String);
|
||||
check(state, String);
|
||||
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'cloud:finishOAuthAuthorization',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(uid, 'manage-cloud'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'cloud:finishOAuthAuthorization',
|
||||
});
|
||||
}
|
||||
|
||||
return finishOAuthAuthorization(code, state);
|
||||
},
|
||||
async 'cloud:checkUserLoggedIn'() {
|
||||
const uid = Meteor.userId();
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'cloud:checkUserLoggedIn',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(uid, 'manage-cloud'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'cloud:checkUserLoggedIn',
|
||||
});
|
||||
}
|
||||
|
||||
return checkUserHasCloudLogin(uid);
|
||||
},
|
||||
async 'cloud:logout'() {
|
||||
const uid = Meteor.userId();
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'cloud:logout',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(uid, 'manage-cloud'))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
|
||||
method: 'cloud:logout',
|
||||
});
|
||||
}
|
||||
|
||||
return userLogout(uid);
|
||||
},
|
||||
});
|
||||
23
apps/meteor/server/meteor-methods/platform/fetchMyKeys.ts
Normal file
23
apps/meteor/server/meteor-methods/platform/fetchMyKeys.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'e2e.fetchMyKeys'(): { public_key?: string; private_key?: string };
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'e2e.fetchMyKeys'() {
|
||||
methodDeprecationLogger.method('e2e.fetchMyKeys', '9.0.0', '/v1/e2e.fetchMyKeys');
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.fetchMyKeys' });
|
||||
}
|
||||
return Users.fetchKeysByUserId(userId);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { TranslationProviderRegistry } from '../../lib/autotranslate/autotranslate';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'autoTranslate.getProviderUiMetadata'(): Record<string, { name: string; displayName: string }>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
'autoTranslate.getProviderUiMetadata'() {
|
||||
if (!Meteor.userId()) {
|
||||
throw new Meteor.Error('error-action-not-allowed', 'Login neccessary', {
|
||||
method: 'autoTranslate.getProviderUiMetadata',
|
||||
});
|
||||
}
|
||||
|
||||
return Object.fromEntries(
|
||||
TranslationProviderRegistry.getProviders().map((provider) => {
|
||||
const { name, displayName } = provider._getProviderMetadata();
|
||||
return [name, { name, displayName }];
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
27
apps/meteor/server/meteor-methods/platform/getStatistics.ts
Normal file
27
apps/meteor/server/meteor-methods/platform/getStatistics.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import type { IStats } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { getLastStatistics } from '../../lib/statistics/functions/getLastStatistics';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
getStatistics(refresh?: boolean): IStats;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async getStatistics(refresh) {
|
||||
methodDeprecationLogger.method('getStatistics', '9.0.0', '/v1/statistics');
|
||||
const uid = Meteor.userId();
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getStatistics' });
|
||||
}
|
||||
return getLastStatistics({
|
||||
userId: uid,
|
||||
refresh,
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { ISupportedLanguage } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { DDPRateLimiter } from 'meteor/ddp-rate-limiter';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { getSupportedLanguages } from '../../lib/autotranslate/functions/getSupportedLanguages';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'autoTranslate.getSupportedLanguages'(targetLanguage: string): ISupportedLanguage[] | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'autoTranslate.getSupportedLanguages'(targetLanguage) {
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'getSupportedLanguages',
|
||||
});
|
||||
}
|
||||
|
||||
return getSupportedLanguages(userId, targetLanguage);
|
||||
},
|
||||
});
|
||||
|
||||
DDPRateLimiter.addRule(
|
||||
{
|
||||
type: 'method',
|
||||
name: 'autoTranslate.getSupportedLanguages',
|
||||
userId(/* userId*/) {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
5,
|
||||
60000,
|
||||
);
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { IRoom, IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Subscriptions, Users } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomIdAsync } from '../../lib/authorization/canAccessRoom';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'e2e.getUsersOfRoomWithoutKey'(rid: IRoom['_id']): { users: Pick<IUser, '_id' | 'e2e'>[] };
|
||||
}
|
||||
}
|
||||
|
||||
export const getUsersOfRoomWithoutKeyMethod = async (
|
||||
userId: string,
|
||||
rid: IRoom['_id'],
|
||||
): Promise<{ users: Pick<IUser, '_id' | 'e2e'>[] }> => {
|
||||
if (!(await canAccessRoomIdAsync(rid, userId))) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.getUsersOfRoomWithoutKey' });
|
||||
}
|
||||
|
||||
const subscriptions = await Subscriptions.findByRidWithoutE2EKey(rid, {
|
||||
projection: { 'u._id': 1 },
|
||||
}).toArray();
|
||||
const userIds = subscriptions.map((s) => s.u._id);
|
||||
const options = { projection: { 'e2e.public_key': 1 } };
|
||||
|
||||
const users = await Users.findByIdsWithPublicE2EKey(userIds, options).toArray();
|
||||
|
||||
return {
|
||||
users,
|
||||
};
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'e2e.getUsersOfRoomWithoutKey'(rid) {
|
||||
check(rid, String);
|
||||
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'e2e.getUsersOfRoomWithoutKey',
|
||||
});
|
||||
}
|
||||
|
||||
if (!rid) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', {
|
||||
method: 'e2e.getUsersOfRoomWithoutKey',
|
||||
});
|
||||
}
|
||||
|
||||
return getUsersOfRoomWithoutKeyMethod(userId, rid);
|
||||
},
|
||||
});
|
||||
24
apps/meteor/server/meteor-methods/platform/loadLocale.ts
Normal file
24
apps/meteor/server/meteor-methods/platform/loadLocale.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { getMomentLocale } from '../../lib/getMomentLocale';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
loadLocale(locale: string): string | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
loadLocale(locale) {
|
||||
check(locale, String);
|
||||
|
||||
try {
|
||||
return getMomentLocale(locale);
|
||||
} catch (error: any) {
|
||||
throw new Meteor.Error(error.message, `Moment locale not found: ${locale}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
80
apps/meteor/server/meteor-methods/platform/push.ts
Normal file
80
apps/meteor/server/meteor-methods/platform/push.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Push } from '@rocket.chat/core-services';
|
||||
import type { IPushToken } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { PushToken } from '@rocket.chat/models';
|
||||
import { Accounts } from 'meteor/accounts-base';
|
||||
import { Match, check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { logger } from '../../lib/notifications/push/logger';
|
||||
import { _matchToken } from '../../lib/notifications/push/push';
|
||||
|
||||
type PushUpdateOptions = {
|
||||
id?: string;
|
||||
token: IPushToken['token'];
|
||||
authToken: string;
|
||||
appName: string;
|
||||
userId: string | null;
|
||||
metadata?: Record<string, unknown>;
|
||||
};
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'raix:push-update'(options: PushUpdateOptions): Promise<Omit<IPushToken, 'authToken'>>;
|
||||
'raix:push-setuser'(options: { id: string; userId: string }): Promise<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'raix:push-update'(options) {
|
||||
methodDeprecationLogger.method('raix:push-update', '9.0.0', '/v1/push.token');
|
||||
logger.debug({ msg: 'Got push token from app', options });
|
||||
|
||||
check(options, {
|
||||
id: Match.Optional(String),
|
||||
token: _matchToken,
|
||||
authToken: String,
|
||||
appName: String,
|
||||
userId: Match.OneOf(String, null),
|
||||
metadata: Match.Optional(Object),
|
||||
});
|
||||
|
||||
// The if user id is set then user id should match on client and connection
|
||||
if (!this.userId || (options.userId && options.userId !== this.userId)) {
|
||||
throw new Meteor.Error(403, 'Forbidden access');
|
||||
}
|
||||
|
||||
// Retain old behavior: if id is not specified but userId is explicitly set, then update the user's first token
|
||||
if (!options.id && options.userId) {
|
||||
const firstDoc = await PushToken.findFirstByUserId(options.userId, { projection: { _id: 1 } });
|
||||
if (firstDoc) {
|
||||
options.id = firstDoc._id;
|
||||
}
|
||||
}
|
||||
|
||||
const authToken = Accounts._hashLoginToken(options.authToken);
|
||||
|
||||
return Push.registerPushToken({
|
||||
...(options.id && { _id: options.id }),
|
||||
token: options.token,
|
||||
appName: options.appName,
|
||||
authToken,
|
||||
userId: this.userId,
|
||||
...(options.metadata && { metadata: options.metadata }),
|
||||
});
|
||||
},
|
||||
async 'raix:push-setuser'(id) {
|
||||
methodDeprecationLogger.method('raix:push-setuser', '9.0.0', []);
|
||||
|
||||
check(id, String);
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error(403, 'Forbidden access');
|
||||
}
|
||||
|
||||
logger.debug({ msg: 'Setting userId for app', userId: this.userId, appId: id });
|
||||
const found = await PushToken.updateOne({ _id: id }, { $set: { userId: this.userId } });
|
||||
|
||||
return !!found;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,123 @@
|
||||
import { mkdtemp } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path, { join } from 'node:path';
|
||||
|
||||
import type { IExportOperation, IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { ExportOperations, UserDataFiles } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import * as dataExport from '../../lib/dataExport';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
requestDataDownload(params: { fullExport?: boolean }): Promise<{
|
||||
requested: boolean;
|
||||
exportOperation: IExportOperation;
|
||||
url: string | null;
|
||||
pendingOperationsBeforeMyRequest: number;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
|
||||
export const requestDataDownload = async ({
|
||||
userData,
|
||||
fullExport = false,
|
||||
}: {
|
||||
userData: IUser;
|
||||
fullExport?: boolean;
|
||||
}): Promise<{
|
||||
requested: boolean;
|
||||
exportOperation: IExportOperation;
|
||||
url: string | null;
|
||||
pendingOperationsBeforeMyRequest: number;
|
||||
}> => {
|
||||
const currentUserData = userData;
|
||||
|
||||
if (!currentUserData) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user');
|
||||
}
|
||||
|
||||
const userId = currentUserData._id;
|
||||
|
||||
const lastOperation = await ExportOperations.findLastOperationByUser(userId, fullExport);
|
||||
const requestDay = lastOperation ? lastOperation.createdAt : new Date();
|
||||
const pendingOperationsBeforeMyRequestCount = await ExportOperations.countAllPendingBeforeMyRequest(requestDay);
|
||||
|
||||
if (lastOperation) {
|
||||
const yesterday = new Date();
|
||||
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
||||
|
||||
if (lastOperation.createdAt > yesterday) {
|
||||
if (lastOperation.status === 'completed') {
|
||||
const file = lastOperation.fileId
|
||||
? await UserDataFiles.findOneById(lastOperation.fileId)
|
||||
: await UserDataFiles.findLastFileByUser(userId);
|
||||
if (file) {
|
||||
return {
|
||||
requested: false,
|
||||
exportOperation: lastOperation,
|
||||
url: dataExport.getPath(file._id),
|
||||
pendingOperationsBeforeMyRequest: pendingOperationsBeforeMyRequestCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
requested: false,
|
||||
exportOperation: lastOperation,
|
||||
url: null,
|
||||
pendingOperationsBeforeMyRequest: pendingOperationsBeforeMyRequestCount,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const tempFolder = settings.get<string | undefined>('UserData_FileSystemPath')?.trim() || (await mkdtemp(join(tmpdir(), 'userData')));
|
||||
|
||||
const exportOperation = {
|
||||
status: 'preparing',
|
||||
userId: currentUserData._id,
|
||||
roomList: undefined,
|
||||
fileList: [],
|
||||
generatedFile: undefined,
|
||||
fullExport,
|
||||
userData: currentUserData,
|
||||
userNameTable: {},
|
||||
} as unknown as IExportOperation; // @todo yikes!
|
||||
|
||||
const id = await ExportOperations.create(exportOperation);
|
||||
exportOperation._id = id;
|
||||
|
||||
const folderName = path.join(tempFolder, id);
|
||||
|
||||
const assetsFolder = path.join(folderName, 'assets');
|
||||
|
||||
exportOperation.exportPath = folderName;
|
||||
exportOperation.assetsPath = assetsFolder;
|
||||
exportOperation.status = 'pending';
|
||||
|
||||
await ExportOperations.updateOperation(exportOperation);
|
||||
|
||||
return {
|
||||
requested: true,
|
||||
exportOperation,
|
||||
url: null,
|
||||
pendingOperationsBeforeMyRequest: pendingOperationsBeforeMyRequestCount,
|
||||
};
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async requestDataDownload({ fullExport = false }) {
|
||||
methodDeprecationLogger.method('requestDataDownload', '9.0.0', '/v1/users.requestDataDownload');
|
||||
const currentUserData = await Meteor.userAsync();
|
||||
|
||||
if (!currentUserData) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user');
|
||||
}
|
||||
|
||||
return requestDataDownload({ userData: currentUserData as IUser, fullExport });
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { api } from '@rocket.chat/core-services';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Subscriptions, Rooms } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'e2e.requestSubscriptionKeys'(): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const requestSubscriptionKeysMethod = async (userId: string): Promise<void> => {
|
||||
// Get all encrypted rooms that the user is subscribed to and has no E2E key yet
|
||||
const subscriptions = await Subscriptions.findByUserIdWithoutE2E(userId).toArray();
|
||||
const roomIds = subscriptions.map((subscription) => subscription.rid);
|
||||
|
||||
// For all subscriptions without E2E key, get the rooms that have encryption enabled
|
||||
const query = {
|
||||
e2eKeyId: {
|
||||
$exists: true,
|
||||
},
|
||||
_id: {
|
||||
$in: roomIds,
|
||||
},
|
||||
};
|
||||
|
||||
const rooms = Rooms.find(query);
|
||||
await rooms.forEach((room) => {
|
||||
void api.broadcast('notify.e2e.keyRequest', room._id, room.e2eKeyId);
|
||||
});
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'e2e.requestSubscriptionKeys'() {
|
||||
methodDeprecationLogger.method('e2e.requestSubscriptionKeys', '9.0.0', '/v1/e2e.requestSubscriptionKeys');
|
||||
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'requestSubscriptionKeys',
|
||||
});
|
||||
}
|
||||
|
||||
await requestSubscriptionKeysMethod(userId);
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
34
apps/meteor/server/meteor-methods/platform/resetOwnE2EKey.ts
Normal file
34
apps/meteor/server/meteor-methods/platform/resetOwnE2EKey.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { MeteorError } from '@rocket.chat/core-services';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { twoFactorRequired } from '../../lib/2fa/twoFactorRequired';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { resetUserE2EEncriptionKey } from '../../lib/resetUserE2EKey';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'e2e.resetOwnE2EKey'(): Promise<boolean>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
'e2e.resetOwnE2EKey': twoFactorRequired(async () => {
|
||||
methodDeprecationLogger.method('e2e.resetOwnE2EKey', '9.0.0', '/v1/users.resetE2EKey');
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new MeteorError('error-invalid-user', 'Invalid user', {
|
||||
method: 'resetOwnE2EKey',
|
||||
});
|
||||
}
|
||||
|
||||
if (!(await resetUserE2EEncriptionKey(userId, false))) {
|
||||
throw new MeteorError('failed-reset-e2e-password', 'Failed to reset E2E password', {
|
||||
method: 'resetOwnE2EKey',
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
});
|
||||
41
apps/meteor/server/meteor-methods/platform/restartServer.ts
Normal file
41
apps/meteor/server/meteor-methods/platform/restartServer.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
restart_server(): {
|
||||
message: string;
|
||||
params: [number];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async restart_server() {
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'restart_server' });
|
||||
}
|
||||
|
||||
if ((await hasPermissionAsync(uid, 'restart-server')) !== true) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'restart_server' });
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setTimeout(() => {
|
||||
console.warn('Call to process.exit() timed out, aborting.');
|
||||
process.abort();
|
||||
}, 1000);
|
||||
process.exit(1);
|
||||
}, 1000);
|
||||
|
||||
return {
|
||||
message: 'The_server_will_restart_in_s_seconds',
|
||||
params: [2],
|
||||
};
|
||||
},
|
||||
});
|
||||
26
apps/meteor/server/meteor-methods/platform/saveSettings.ts
Normal file
26
apps/meteor/server/meteor-methods/platform/saveSettings.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { saveAutoTranslateSettings } from '../../lib/autotranslate/functions/saveSettings';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'autoTranslate.saveSettings'(rid: string, field: string, value: string, options: { defaultLanguage: string }): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'autoTranslate.saveSettings'(rid, field, value, options) {
|
||||
methodDeprecationLogger.method('autoTranslate.saveSettings', '9.0.0', '/v1/autotranslate.saveSettings');
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'saveAutoTranslateSettings',
|
||||
});
|
||||
}
|
||||
|
||||
return saveAutoTranslateSettings(userId, rid, field, value, options);
|
||||
},
|
||||
});
|
||||
94
apps/meteor/server/meteor-methods/platform/search.ts
Normal file
94
apps/meteor/server/meteor-methods/platform/search.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { IMessageSearchProvider, IMessageSearchSuggestion, IRoom, IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { SearchLogger } from '../../lib/search/logger/logger';
|
||||
import type { IRawSearchResult, ISearchResult } from '../../lib/search/model/ISearchResult';
|
||||
import { searchProviderService, validationService } from '../../lib/search/service';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'rocketchatSearch.getProvider'(): IMessageSearchProvider | undefined;
|
||||
'rocketchatSearch.search'(text: string, context: { uid?: IUser['_id']; rid: IRoom['_id'] }, payload: unknown): Promise<ISearchResult>;
|
||||
'rocketchatSearch.suggest'(
|
||||
text: string,
|
||||
context: { uid?: IUser['_id']; rid: IRoom['_id'] },
|
||||
payload: unknown,
|
||||
): Promise<IMessageSearchSuggestion[]>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
/**
|
||||
* Get the current provider with key, description, resultTemplate, suggestionItemTemplate and settings (as Map)
|
||||
*/
|
||||
'rocketchatSearch.getProvider'() {
|
||||
const provider = searchProviderService.activeProvider;
|
||||
if (!provider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
key: provider.key,
|
||||
description: provider.i18nDescription,
|
||||
icon: provider.iconName,
|
||||
resultTemplate: provider.resultTemplate,
|
||||
supportsSuggestions: provider.supportsSuggestions,
|
||||
suggestionItemTemplate: provider.suggestionItemTemplate,
|
||||
settings: Object.fromEntries(Object.values(provider.settingsAsMap).map((setting) => [setting.key, setting.value] as const)),
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Search using the current search provider and check if results are valid for the user. The search result has
|
||||
* the format `{messages:{start:0,numFound:1,docs:[{...}]},users:{...},rooms:{...}}`
|
||||
* @param text the search text
|
||||
* @param context the context (uid, rid)
|
||||
* @param payload custom payload (e.g. for paging)
|
||||
*/
|
||||
async 'rocketchatSearch.search'(text, context, payload) {
|
||||
payload = payload !== null ? payload : undefined; // TODO is this cleanup necessary?
|
||||
|
||||
if (!searchProviderService.activeProvider) {
|
||||
throw new Error('Provider currently not active');
|
||||
}
|
||||
|
||||
SearchLogger.debug({ msg: 'search', text, context, payload });
|
||||
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Error('User not logged in');
|
||||
}
|
||||
|
||||
return new Promise<IRawSearchResult>((resolve, reject) => {
|
||||
void searchProviderService.activeProvider?.search(userId, text, context, payload, (error, data) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
return resolve(data);
|
||||
});
|
||||
}).then((result) => validationService.validateSearchResult(result));
|
||||
},
|
||||
|
||||
async 'rocketchatSearch.suggest'(text, context, payload) {
|
||||
payload ??= undefined; // TODO is this cleanup necessary?
|
||||
|
||||
if (!searchProviderService.activeProvider) {
|
||||
throw new Error('Provider currently not active');
|
||||
}
|
||||
|
||||
SearchLogger.debug({ msg: 'suggest', text, context, payload });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
searchProviderService.activeProvider?.suggest(text, context, payload, (error, data) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
49
apps/meteor/server/meteor-methods/platform/setRoomKeyID.ts
Normal file
49
apps/meteor/server/meteor-methods/platform/setRoomKeyID.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Rooms } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomIdAsync } from '../../lib/authorization/canAccessRoom';
|
||||
import { notifyOnRoomChanged } from '../../lib/notifyListener';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'e2e.setRoomKeyID'(rid: IRoom['_id'], keyID: string): void;
|
||||
}
|
||||
}
|
||||
|
||||
export const setRoomKeyIDMethod = async (userId: string, rid: IRoom['_id'], keyID: string): Promise<void> => {
|
||||
if (!(await canAccessRoomIdAsync(rid, userId))) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' });
|
||||
}
|
||||
|
||||
const room = await Rooms.setE2eKeyId(rid, keyID);
|
||||
|
||||
if (!room) {
|
||||
throw new Meteor.Error('error-room-e2e-key-already-exists', 'E2E Key ID already exists', {
|
||||
method: 'e2e.setRoomKeyID',
|
||||
});
|
||||
}
|
||||
|
||||
void notifyOnRoomChanged(room);
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'e2e.setRoomKeyID'(rid, keyID) {
|
||||
check(rid, String);
|
||||
check(keyID, String);
|
||||
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.setRoomKeyID' });
|
||||
}
|
||||
|
||||
if (!rid) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' });
|
||||
}
|
||||
|
||||
await setRoomKeyIDMethod(userId, rid, keyID);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Rooms, Users } from '@rocket.chat/models';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { notifyOnRoomChangedById } from '../../lib/notifyListener';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'e2e.setUserPublicAndPrivateKeys'({ public_key, private_key }: { public_key: string; private_key: string; force?: boolean }): void;
|
||||
}
|
||||
}
|
||||
|
||||
const isKeysResult = (result: any): result is { public_key: string; private_key: string } => {
|
||||
return result.private_key && result.public_key;
|
||||
};
|
||||
|
||||
export const setUserPublicAndPrivateKeysMethod = async (
|
||||
userId: string,
|
||||
keyPair: { public_key: string; private_key: string; force?: boolean },
|
||||
): Promise<void> => {
|
||||
if (!keyPair.force) {
|
||||
const keys = await Users.fetchKeysByUserId(userId);
|
||||
|
||||
if (isKeysResult(keys)) {
|
||||
throw new Meteor.Error('error-keys-already-set', 'Keys already set', {
|
||||
method: 'e2e.setUserPublicAndPrivateKeys',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await Users.setE2EPublicAndPrivateKeysByUserId(userId, {
|
||||
private_key: keyPair.private_key,
|
||||
public_key: keyPair.public_key,
|
||||
});
|
||||
|
||||
const subscribedRoomIds = await Rooms.getSubscribedRoomIdsWithoutE2EKeys(userId);
|
||||
await Rooms.addUserIdToE2EEQueueByRoomIds(subscribedRoomIds, userId);
|
||||
|
||||
void notifyOnRoomChangedById(subscribedRoomIds);
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'e2e.setUserPublicAndPrivateKeys'(keyPair) {
|
||||
methodDeprecationLogger.method('e2e.setUserPublicAndPrivateKeys', '9.0.0', '/v1/e2e.setUserPublicAndPrivateKeys');
|
||||
const userId = Meteor.userId();
|
||||
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'e2e.setUserPublicAndPrivateKeys',
|
||||
});
|
||||
}
|
||||
|
||||
if (!keyPair.public_key || !keyPair.private_key) {
|
||||
throw new Meteor.Error('error-invalid-keys', 'Invalid keys', {
|
||||
method: 'e2e.setUserPublicAndPrivateKeys',
|
||||
});
|
||||
}
|
||||
|
||||
await setUserPublicAndPrivateKeysMethod(userId, keyPair);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { IMessage } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Messages, Rooms } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { canAccessRoomAsync } from '../../lib/authorization';
|
||||
import { translateMessage } from '../../lib/autotranslate/functions/translateMessage';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
'autoTranslate.translateMessage'(message: IMessage | undefined, targetLanguage: string): Promise<IMessage | undefined>;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async 'autoTranslate.translateMessage'(message, targetLanguage) {
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'autoTranslate.translateMessage',
|
||||
});
|
||||
}
|
||||
check(message?._id, String);
|
||||
check(targetLanguage, String);
|
||||
const msg = await Messages.findOneById(message._id);
|
||||
if (!msg) {
|
||||
throw new Meteor.Error('error-message-not-found', 'Message not found');
|
||||
}
|
||||
const room = await Rooms.findOneById(msg.rid);
|
||||
if (!room || !(await canAccessRoomAsync(room, { _id: userId }))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed');
|
||||
}
|
||||
return translateMessage(targetLanguage, msg);
|
||||
},
|
||||
});
|
||||
34
apps/meteor/server/meteor-methods/platform/updateGroupKey.ts
Normal file
34
apps/meteor/server/meteor-methods/platform/updateGroupKey.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Subscriptions, Rooms } from '@rocket.chat/models';
|
||||
|
||||
import { notifyOnSubscriptionChangedById, notifyOnSubscriptionChanged, notifyOnRoomChangedById } from '../../lib/notifyListener';
|
||||
|
||||
export async function updateGroupKey(rid: string, uid: string, key: string, callerUserId: string) {
|
||||
// I have a subscription to this room
|
||||
const mySub = await Subscriptions.findOneByRoomIdAndUserId(rid, callerUserId);
|
||||
|
||||
if (mySub) {
|
||||
// Setting the key to myself, can set directly to the final field
|
||||
if (callerUserId === uid) {
|
||||
const setGroupE2EKeyResponse = await Subscriptions.setGroupE2EKey(mySub._id, key);
|
||||
// Case: I create an encrypted room before setting up my keys, and I reset the e2e keys
|
||||
// Next login, I'll create the keys for the room, and set them here.
|
||||
// However as I reset my keys, I'm on the `usersWaitingForKeys` queue
|
||||
// So I need to remove myself from the queue and notify the time i reach here
|
||||
// This way, I can provide the keys to other users
|
||||
const { modifiedCount } = await Rooms.removeUsersFromE2EEQueueByRoomId(mySub.rid, [callerUserId]);
|
||||
if (setGroupE2EKeyResponse.modifiedCount) {
|
||||
void notifyOnSubscriptionChangedById(mySub._id);
|
||||
}
|
||||
if (modifiedCount) {
|
||||
void notifyOnRoomChangedById(mySub.rid);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// uid also has subscription to this room
|
||||
const value = await Subscriptions.setGroupE2ESuggestedKey(uid, rid, key);
|
||||
if (value) {
|
||||
void notifyOnSubscriptionChanged(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
98
apps/meteor/server/meteor-methods/rooms/addAllUserToRoom.ts
Normal file
98
apps/meteor/server/meteor-methods/rooms/addAllUserToRoom.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { Message } from '@rocket.chat/core-services';
|
||||
import type { IRoom } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Subscriptions, Rooms, Users } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { beforeAddUserToRoom } from '../../hooks/rooms/beforeAddUserToRoom';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { callbacks } from '../../lib/callbacks';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { getSubscriptionAutotranslateDefaultConfig } from '../../lib/getSubscriptionAutotranslateDefaultConfig';
|
||||
import { notifyOnSubscriptionChangedById } from '../../lib/notifyListener';
|
||||
import { getDefaultSubscriptionPref } from '../../lib/utils/lib/getDefaultSubscriptionPref';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
addAllUserToRoom(rid: IRoom['_id'], activeUsersOnly?: boolean): Promise<true>;
|
||||
}
|
||||
}
|
||||
|
||||
export const addAllUserToRoomFn = async (userId: string, rid: IRoom['_id'], activeUsersOnly = false): Promise<true> => {
|
||||
check(rid, String);
|
||||
check(activeUsersOnly, Boolean);
|
||||
|
||||
if (!(await hasPermissionAsync(userId, 'add-all-to-room'))) {
|
||||
throw new Meteor.Error(403, 'Access to Method Forbidden', {
|
||||
method: 'addAllToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
const userFilter: {
|
||||
active?: boolean;
|
||||
} = {};
|
||||
if (activeUsersOnly === true) {
|
||||
userFilter.active = true;
|
||||
}
|
||||
|
||||
const users = await Users.find(userFilter).toArray();
|
||||
if (users.length > settings.get<number>('API_User_Limit')) {
|
||||
throw new Meteor.Error('error-user-limit-exceeded', 'User Limit Exceeded', {
|
||||
method: 'addAllToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneById(rid);
|
||||
if (!room) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', {
|
||||
method: 'addAllToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
await beforeAddUserToRoom(
|
||||
users.map((u) => u.username!),
|
||||
room,
|
||||
);
|
||||
|
||||
const now = new Date();
|
||||
for await (const user of users) {
|
||||
const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, user._id);
|
||||
if (subscription != null) {
|
||||
continue;
|
||||
}
|
||||
await callbacks.run('beforeJoinRoom', user, room);
|
||||
const autoTranslateConfig = getSubscriptionAutotranslateDefaultConfig(user);
|
||||
const { insertedId } = await Subscriptions.createWithRoomAndUser(room, user, {
|
||||
ts: now,
|
||||
open: true,
|
||||
alert: true,
|
||||
unread: 1,
|
||||
userMentions: 1,
|
||||
groupMentions: 0,
|
||||
...autoTranslateConfig,
|
||||
...getDefaultSubscriptionPref(user),
|
||||
});
|
||||
if (insertedId) {
|
||||
void notifyOnSubscriptionChangedById(insertedId, 'inserted');
|
||||
}
|
||||
await Message.saveSystemMessage('uj', rid, user.username || '', user, { ts: now });
|
||||
await callbacks.run('afterJoinRoom', user, room);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async addAllUserToRoom(rid, activeUsersOnly = false) {
|
||||
methodDeprecationLogger.method('addAllUserToRoom', '9.0.0', ['/v1/channels.addAll', '/v1/groups.addAll']);
|
||||
if (!this.userId) {
|
||||
throw new Meteor.Error(403, 'Access to Method Forbidden', {
|
||||
method: 'addAllToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
return addAllUserToRoomFn(this.userId, rid, activeUsersOnly);
|
||||
},
|
||||
});
|
||||
103
apps/meteor/server/meteor-methods/rooms/addRoomLeader.ts
Normal file
103
apps/meteor/server/meteor-methods/rooms/addRoomLeader.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { api, Message, Team } from '@rocket.chat/core-services';
|
||||
import type { IRoom, IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Subscriptions, Users } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { notifyOnSubscriptionChangedById } from '../../lib/notifyListener';
|
||||
import { syncRoomRolePriorityForUserAndRoom } from '../../lib/roles/syncRoomRolePriority';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
addRoomLeader(rid: IRoom['_id'], userId: IUser['_id']): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const addRoomLeader = async (fromUserId: IUser['_id'], rid: IRoom['_id'], userId: IUser['_id']): Promise<boolean> => {
|
||||
check(rid, String);
|
||||
check(userId, String);
|
||||
|
||||
if (!(await hasPermissionAsync(fromUserId, 'set-leader', rid))) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
|
||||
method: 'addRoomLeader',
|
||||
});
|
||||
}
|
||||
|
||||
const user = await Users.findOneById(userId);
|
||||
|
||||
if (!user?.username) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addRoomLeader',
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, user._id);
|
||||
|
||||
if (!subscription) {
|
||||
throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', {
|
||||
method: 'addRoomLeader',
|
||||
});
|
||||
}
|
||||
|
||||
if (subscription.roles && Array.isArray(subscription.roles) === true && subscription.roles.includes('leader') === true) {
|
||||
throw new Meteor.Error('error-user-already-leader', 'User is already a leader', {
|
||||
method: 'addRoomLeader',
|
||||
});
|
||||
}
|
||||
|
||||
const addRoleResponse = await Subscriptions.addRoleById(subscription._id, 'leader');
|
||||
await syncRoomRolePriorityForUserAndRoom(userId, rid, subscription.roles?.concat(['leader']) || ['leader']);
|
||||
|
||||
if (addRoleResponse.modifiedCount) {
|
||||
void notifyOnSubscriptionChangedById(subscription._id);
|
||||
}
|
||||
|
||||
const fromUser = await Users.findOneById(fromUserId);
|
||||
|
||||
if (!fromUser) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addRoomLeader',
|
||||
});
|
||||
}
|
||||
|
||||
await Message.saveSystemMessage('subscription-role-added', rid, user.username, fromUser, { role: 'leader' });
|
||||
|
||||
const team = await Team.getOneByMainRoomId(rid);
|
||||
if (team) {
|
||||
await Team.addRolesToMember(team._id, userId, ['leader']);
|
||||
}
|
||||
|
||||
if (settings.get<boolean>('UI_DisplayRoles')) {
|
||||
void api.broadcast('user.roleUpdate', {
|
||||
type: 'added',
|
||||
_id: 'leader',
|
||||
u: {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
},
|
||||
scope: rid,
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async addRoomLeader(rid, userId) {
|
||||
methodDeprecationLogger.method('addRoomLeader', '9.0.0', ['/v1/channels.addLeader', '/v1/groups.addLeader']);
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addRoomLeader',
|
||||
});
|
||||
}
|
||||
|
||||
return addRoomLeader(uid, rid, userId);
|
||||
},
|
||||
});
|
||||
125
apps/meteor/server/meteor-methods/rooms/addRoomModerator.ts
Normal file
125
apps/meteor/server/meteor-methods/rooms/addRoomModerator.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { api, Message, Team } from '@rocket.chat/core-services';
|
||||
import type { IRoom, IUser } from '@rocket.chat/core-typings';
|
||||
import { isRoomFederated } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Subscriptions, Rooms, Users } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { beforeChangeRoomRole } from '../../lib/callbacks/beforeChangeRoomRole';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { notifyOnSubscriptionChangedById } from '../../lib/notifyListener';
|
||||
import { syncRoomRolePriorityForUserAndRoom } from '../../lib/roles/syncRoomRolePriority';
|
||||
import { isFederationEnabled, FederationMatrixInvalidConfigurationError } from '../../services/federation/utils';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
addRoomModerator(rid: IRoom['_id'], userId: IUser['_id']): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const addRoomModerator = async (fromUserId: IUser['_id'], rid: IRoom['_id'], userId: IUser['_id']): Promise<boolean> => {
|
||||
check(rid, String);
|
||||
check(userId, String);
|
||||
|
||||
const room = await Rooms.findOneById(rid, { projection: { t: 1, federated: 1, federation: 1 } });
|
||||
if (!room) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', {
|
||||
method: 'addRoomModerator',
|
||||
});
|
||||
}
|
||||
|
||||
const isFederated = isRoomFederated(room);
|
||||
|
||||
if (!(await hasPermissionAsync(fromUserId, 'set-moderator', rid)) && !isFederated) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
|
||||
method: 'addRoomModerator',
|
||||
});
|
||||
}
|
||||
|
||||
if (isFederated && !isFederationEnabled()) {
|
||||
throw new FederationMatrixInvalidConfigurationError('unable to change room owners');
|
||||
}
|
||||
|
||||
const user = await Users.findOneById(userId);
|
||||
|
||||
if (!user?.username) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addRoomModerator',
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, user._id);
|
||||
|
||||
if (!subscription) {
|
||||
throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', {
|
||||
method: 'addRoomModerator',
|
||||
});
|
||||
}
|
||||
|
||||
if (subscription.roles && Array.isArray(subscription.roles) === true && subscription.roles.includes('moderator') === true) {
|
||||
throw new Meteor.Error('error-user-already-moderator', 'User is already a moderator', {
|
||||
method: 'addRoomModerator',
|
||||
});
|
||||
}
|
||||
|
||||
await beforeChangeRoomRole.run({ fromUserId, userId, room, role: 'moderator' });
|
||||
|
||||
const addRoleResponse = await Subscriptions.addRoleById(subscription._id, 'moderator');
|
||||
await syncRoomRolePriorityForUserAndRoom(userId, rid, subscription.roles?.concat(['moderator']) || ['moderator']);
|
||||
|
||||
if (addRoleResponse.modifiedCount) {
|
||||
void notifyOnSubscriptionChangedById(subscription._id);
|
||||
}
|
||||
|
||||
const fromUser = await Users.findOneById(fromUserId);
|
||||
if (!fromUser) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addRoomLeader',
|
||||
});
|
||||
}
|
||||
|
||||
await Message.saveSystemMessage('subscription-role-added', rid, user.username, fromUser, { role: 'moderator' });
|
||||
|
||||
const team = await Team.getOneByMainRoomId(rid);
|
||||
if (team) {
|
||||
await Team.addRolesToMember(team._id, userId, ['moderator']);
|
||||
}
|
||||
|
||||
const event = {
|
||||
type: 'added',
|
||||
_id: 'moderator',
|
||||
u: {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
name: fromUser.name,
|
||||
},
|
||||
scope: rid,
|
||||
} as const;
|
||||
|
||||
if (settings.get<boolean>('UI_DisplayRoles')) {
|
||||
void api.broadcast('user.roleUpdate', event);
|
||||
}
|
||||
|
||||
void api.broadcast('federation.userRoleChanged', { ...event, givenByUserId: fromUserId });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async addRoomModerator(rid, userId) {
|
||||
methodDeprecationLogger.method('addRoomModerator', '9.0.0', ['/v1/channels.addModerator', '/v1/groups.addModerator']);
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addRoomModerator',
|
||||
});
|
||||
}
|
||||
|
||||
return addRoomModerator(uid, rid, userId);
|
||||
},
|
||||
});
|
||||
122
apps/meteor/server/meteor-methods/rooms/addRoomOwner.ts
Normal file
122
apps/meteor/server/meteor-methods/rooms/addRoomOwner.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { api, Message, Team } from '@rocket.chat/core-services';
|
||||
import type { IRoom, IUser } from '@rocket.chat/core-typings';
|
||||
import { isRoomFederated } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Subscriptions, Rooms, Users } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { beforeChangeRoomRole } from '../../lib/callbacks/beforeChangeRoomRole';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { notifyOnSubscriptionChangedById } from '../../lib/notifyListener';
|
||||
import { syncRoomRolePriorityForUserAndRoom } from '../../lib/roles/syncRoomRolePriority';
|
||||
import { isFederationEnabled, FederationMatrixInvalidConfigurationError } from '../../services/federation/utils';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
addRoomOwner(rid: IRoom['_id'], userId: IUser['_id']): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const addRoomOwner = async (fromUserId: IUser['_id'], rid: IRoom['_id'], userId: IUser['_id']): Promise<boolean> => {
|
||||
check(rid, String);
|
||||
check(userId, String);
|
||||
|
||||
const room = await Rooms.findOneById(rid, { projection: { t: 1, federated: 1, federation: 1 } });
|
||||
if (!room) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', {
|
||||
method: 'addRoomOwner',
|
||||
});
|
||||
}
|
||||
|
||||
const isFederated = isRoomFederated(room);
|
||||
|
||||
if (!(await hasPermissionAsync(fromUserId, 'set-owner', rid)) && !isFederated) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
|
||||
method: 'addRoomOwner',
|
||||
});
|
||||
}
|
||||
|
||||
if (isFederated && !isFederationEnabled()) {
|
||||
throw new FederationMatrixInvalidConfigurationError('unable to change room owners');
|
||||
}
|
||||
|
||||
const user = await Users.findOneById(userId);
|
||||
|
||||
if (!user?.username) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addRoomOwner',
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, user._id);
|
||||
|
||||
if (!subscription) {
|
||||
throw new Meteor.Error('error-user-not-in-room', 'User is not in this room', {
|
||||
method: 'addRoomOwner',
|
||||
});
|
||||
}
|
||||
|
||||
if (subscription.roles && Array.isArray(subscription.roles) === true && subscription.roles.includes('owner') === true) {
|
||||
throw new Meteor.Error('error-user-already-owner', 'User is already an owner', {
|
||||
method: 'addRoomOwner',
|
||||
});
|
||||
}
|
||||
|
||||
await beforeChangeRoomRole.run({ fromUserId, userId, room, role: 'owner' });
|
||||
|
||||
const addRoleResponse = await Subscriptions.addRoleById(subscription._id, 'owner');
|
||||
await syncRoomRolePriorityForUserAndRoom(userId, rid, subscription.roles?.concat(['owner']) || ['owner']);
|
||||
|
||||
if (addRoleResponse.modifiedCount) {
|
||||
void notifyOnSubscriptionChangedById(subscription._id);
|
||||
}
|
||||
|
||||
const fromUser = await Users.findOneById(fromUserId);
|
||||
if (!fromUser) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addRoomLeader',
|
||||
});
|
||||
}
|
||||
|
||||
await Message.saveSystemMessage('subscription-role-added', rid, user.username, fromUser, { role: 'owner' });
|
||||
|
||||
const team = await Team.getOneByMainRoomId(rid);
|
||||
if (team) {
|
||||
await Team.addRolesToMember(team._id, userId, ['owner']);
|
||||
}
|
||||
const event = {
|
||||
type: 'added',
|
||||
_id: 'owner',
|
||||
u: {
|
||||
_id: user._id,
|
||||
username: user.username,
|
||||
name: user.name,
|
||||
},
|
||||
scope: rid,
|
||||
} as const;
|
||||
if (settings.get('UI_DisplayRoles')) {
|
||||
void api.broadcast('user.roleUpdate', event);
|
||||
}
|
||||
void api.broadcast('federation.userRoleChanged', { ...event, givenByUserId: fromUserId });
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async addRoomOwner(rid, userId) {
|
||||
methodDeprecationLogger.method('addRoomOwner', '9.0.0', ['/v1/channels.addOwner', '/v1/groups.addOwner']);
|
||||
const uid = Meteor.userId();
|
||||
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addRoomOwner',
|
||||
});
|
||||
}
|
||||
|
||||
return addRoomOwner(uid, rid, userId);
|
||||
},
|
||||
});
|
||||
29
apps/meteor/server/meteor-methods/rooms/addUserToRoom.ts
Normal file
29
apps/meteor/server/meteor-methods/rooms/addUserToRoom.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { addUsersToRoomMethod } from './addUsersToRoom';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
addUserToRoom(data: { rid: string; username: string }): void;
|
||||
}
|
||||
}
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async addUserToRoom(data) {
|
||||
methodDeprecationLogger.method('addUserToRoom', '9.0.0', ['/v1/channels.invite', '/v1/groups.invite']);
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addUserToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
await addUsersToRoomMethod(userId, {
|
||||
rid: data.rid,
|
||||
users: [data.username],
|
||||
});
|
||||
},
|
||||
});
|
||||
137
apps/meteor/server/meteor-methods/rooms/addUsersToRoom.ts
Normal file
137
apps/meteor/server/meteor-methods/rooms/addUsersToRoom.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { api } from '@rocket.chat/core-services';
|
||||
import { isBannedSubscription, isRoomNativeFederated, type IUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Subscriptions, Users, Rooms } from '@rocket.chat/models';
|
||||
import { Match } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { beforeAddUsersToRoom } from '../../lib/callbacks/beforeAddUserToRoom';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { i18n } from '../../lib/i18n';
|
||||
import { addUserToRoom } from '../../lib/rooms/addUserToRoom';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
addUsersToRoom(data: { rid: string; users: string[] }): boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export const sanitizeUsername = (username: string) => {
|
||||
const isFederatedUsername = username.includes('@') && username.includes(':');
|
||||
if (isFederatedUsername) {
|
||||
return username;
|
||||
}
|
||||
|
||||
return username.replace(/(^@)|( @)/, '');
|
||||
};
|
||||
|
||||
export const addUsersToRoomMethod = async (userId: string, data: { rid: string; users: string[] }, user?: IUser): Promise<boolean> => {
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addUsersToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
if (!Match.test(data.rid, String)) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', {
|
||||
method: 'addUsersToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
// Get user and room details
|
||||
const room = await Rooms.findOneById(data.rid);
|
||||
if (!room) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', {
|
||||
method: 'addUsersToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = await Subscriptions.findOneByRoomIdAndUserId(data.rid, userId, {
|
||||
projection: { _id: 1 },
|
||||
});
|
||||
const userInRoom = subscription != null;
|
||||
|
||||
if (room.t === 'd' && !isRoomNativeFederated(room)) {
|
||||
throw new Meteor.Error('error-cant-invite-for-direct-room', "Can't invite user to direct rooms", {
|
||||
method: 'addUsersToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
// Can add to any room you're in, with permission, otherwise need specific room type permission
|
||||
let canAddUser = false;
|
||||
if (userInRoom && (await hasPermissionAsync(userId, 'add-user-to-joined-room', room._id))) {
|
||||
canAddUser = true;
|
||||
} else if (room.t === 'c' && (await hasPermissionAsync(userId, 'add-user-to-any-c-room'))) {
|
||||
canAddUser = true;
|
||||
} else if (room.t === 'p' && (await hasPermissionAsync(userId, 'add-user-to-any-p-room'))) {
|
||||
canAddUser = true;
|
||||
}
|
||||
|
||||
// Adding wasn't allowed
|
||||
if (!canAddUser) {
|
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
|
||||
method: 'addUsersToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
// Missing the users to be added
|
||||
if (!Array.isArray(data.users)) {
|
||||
throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', {
|
||||
method: 'addUsersToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
await beforeAddUsersToRoom.run({ usernames: data.users, inviter: user }, room);
|
||||
|
||||
await Promise.all(
|
||||
data.users.map(async (username) => {
|
||||
const sanitizedUsername = sanitizeUsername(username);
|
||||
|
||||
const newUser = await Users.findOneByUsernameIgnoringCase(sanitizedUsername);
|
||||
if (!newUser) {
|
||||
throw new Meteor.Error('error-user-not-found', 'User not found', {
|
||||
method: 'addUsersToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
const subscription = await Subscriptions.findOneByRoomIdAndUserId(data.rid, newUser._id);
|
||||
if (subscription && isBannedSubscription(subscription)) {
|
||||
throw new Meteor.Error('error-user-is-banned', 'User is banned from this room', {
|
||||
method: 'addUsersToRoom',
|
||||
});
|
||||
}
|
||||
if (!subscription) {
|
||||
return addUserToRoom(data.rid, newUser, user);
|
||||
}
|
||||
if (!newUser.username) {
|
||||
return;
|
||||
}
|
||||
void api.broadcast('notify.ephemeralMessage', userId, data.rid, {
|
||||
msg: i18n.t('Username_is_already_in_here', {
|
||||
interpolation: { escapeValue: false },
|
||||
username: newUser.username,
|
||||
lng: user?.language,
|
||||
}),
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async addUsersToRoom(data) {
|
||||
methodDeprecationLogger.method('addUsersToRoom', '9.0.0', ['/v1/channels.invite', '/v1/groups.invite']);
|
||||
const uid = Meteor.userId();
|
||||
// Validate user and room
|
||||
if (!uid) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', {
|
||||
method: 'addUsersToRoom',
|
||||
});
|
||||
}
|
||||
|
||||
return addUsersToRoomMethod(uid, data, ((await Meteor.userAsync()) as IUser | null) ?? undefined);
|
||||
},
|
||||
});
|
||||
54
apps/meteor/server/meteor-methods/rooms/archiveRoom.ts
Normal file
54
apps/meteor/server/meteor-methods/rooms/archiveRoom.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { isRegisterUser } from '@rocket.chat/core-typings';
|
||||
import type { ServerMethods } from '@rocket.chat/ddp-client';
|
||||
import { Users, Rooms } from '@rocket.chat/models';
|
||||
import { check } from 'meteor/check';
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
|
||||
import { RoomMemberActions } from '../../../definition/IRoomTypeConfig';
|
||||
import { hasPermissionAsync } from '../../lib/authorization/hasPermission';
|
||||
import { methodDeprecationLogger } from '../../lib/deprecationWarningLogger';
|
||||
import { archiveRoom } from '../../lib/rooms/archiveRoom';
|
||||
import { roomCoordinator } from '../../lib/rooms/roomCoordinator';
|
||||
|
||||
declare module '@rocket.chat/ddp-client' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface ServerMethods {
|
||||
archiveRoom(rid: string): Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
export const executeArchiveRoom = async (userId: string, rid: string) => {
|
||||
check(rid, String);
|
||||
|
||||
const user = await Users.findOneById(userId, { projection: { username: 1, name: 1 } });
|
||||
if (!user || !isRegisterUser(user)) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'archiveRoom' });
|
||||
}
|
||||
|
||||
const room = await Rooms.findOneById(rid);
|
||||
if (!room) {
|
||||
throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'archiveRoom' });
|
||||
}
|
||||
|
||||
if (!(await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.ARCHIVE, userId))) {
|
||||
throw new Meteor.Error('error-direct-message-room', `rooms type: ${room.t} can not be archived`, { method: 'archiveRoom' });
|
||||
}
|
||||
|
||||
if (!(await hasPermissionAsync(userId, 'archive-room', room._id))) {
|
||||
throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'archiveRoom' });
|
||||
}
|
||||
|
||||
return archiveRoom(rid, user);
|
||||
};
|
||||
|
||||
Meteor.methods<ServerMethods>({
|
||||
async archiveRoom(rid) {
|
||||
methodDeprecationLogger.method('archiveRoom', '9.0.0', '/v1/channels.archive');
|
||||
const userId = Meteor.userId();
|
||||
if (!userId) {
|
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'archiveRoom' });
|
||||
}
|
||||
|
||||
return executeArchiveRoom(userId, rid);
|
||||
},
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user