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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,11 @@ export const insertOrUpdateUserStatus = async (userId: string, userStatusData: I
});
}

let matchingResults = [];
const conflictingUserStatus = userStatusData._id
? await CustomUserStatus.findOneByNameExceptId(userStatusData.name, userStatusData._id, { projection: { _id: 1 } })
: await CustomUserStatus.findOneByName(userStatusData.name, { projection: { _id: 1 } });

if (userStatusData._id) {
matchingResults = await CustomUserStatus.findByNameExceptId(userStatusData.name, userStatusData._id).toArray();
} else {
matchingResults = await CustomUserStatus.findByName(userStatusData.name).toArray();
}

if (matchingResults.length > 0) {
if (conflictingUserStatus) {
throw new Meteor.Error('Custom_User_Status_Error_Name_Already_In_Use', 'The custom user status name is already in use', {
method: 'insertOrUpdateUserStatus',
});
Expand Down
8 changes: 2 additions & 6 deletions apps/meteor/server/api/v1/omnichannel/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,8 @@ API.v1.addRoute(
let rid: string;
if (visitor) {
const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { userId: this.userId });
const rooms = await LivechatRooms.findOpenByVisitorToken(visitorToken, {}, extraQuery).toArray();
if (rooms && rooms.length > 0) {
rid = rooms[0]._id;
} else {
rid = Random.id();
}
const room = await LivechatRooms.findOneOpenByVisitorToken(visitorToken, { projection: { _id: 1 } }, extraQuery);
rid = room?._id ?? Random.id();
} else {
rid = Random.id();

Expand Down
8 changes: 4 additions & 4 deletions apps/meteor/server/api/v1/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,17 @@ const permissionsEndpoints = API.v1
}

const permissionKeys = bodyParams.permissions.map(({ _id }) => _id);
const permissions = await Permissions.find({ _id: { $in: permissionKeys } }).toArray();
const permissionsCount = await Permissions.countDocuments({ _id: { $in: permissionKeys } });

if (permissions.length !== bodyParams.permissions.length) {
if (permissionsCount !== bodyParams.permissions.length) {
return API.v1.failure('Invalid permission', 'error-invalid-permission');
}

const roleKeys = [...new Set(bodyParams.permissions.flatMap((p) => p.roles))];

const roles = await Roles.find({ _id: { $in: roleKeys } }).toArray();
const rolesCount = await Roles.countDocuments({ _id: { $in: roleKeys } });

if (roles.length !== roleKeys.length) {
if (rolesCount !== roleKeys.length) {
return API.v1.failure('Invalid role', 'error-invalid-role');
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export const insertOrUpdateSound = async (soundData: ICustomSoundData): Promise<
});
}

const matchingResults = await CustomSounds.findByName(soundData.name, soundData._id).toArray();
const conflictingSound = await CustomSounds.findOneByName(soundData.name, soundData._id, { projection: { _id: 1 } });

if (matchingResults.length > 0) {
if (conflictingSound) {
throw new Meteor.Error('Custom_Sound_Error_Name_Already_In_Use', 'The custom sound name is already in use', {
method: 'insertOrUpdateSound',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,21 +68,11 @@ export async function insertOrUpdateEmoji(userId: string | null, emojiData: Emoj

emojiData.extension = emojiData.extension === 'svg+xml' ? 'png' : emojiData.extension;

let matchingResults = [];
const conflictingEmoji = await EmojiCustom.findOneByNamesOrAliases([emojiData.name, ...aliases], emojiData._id, {
projection: { _id: 1 },
});

if (emojiData._id) {
matchingResults = await EmojiCustom.findByNameOrAliasExceptID(emojiData.name, emojiData._id).toArray();
for (const alias of aliases) {
matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAliasExceptID(alias, emojiData._id).toArray());
}
} else {
matchingResults = await EmojiCustom.findByNameOrAlias(emojiData.name).toArray();
for (const alias of aliases) {
matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAlias(alias).toArray());
}
}

if (matchingResults.length > 0) {
if (conflictingEmoji) {
throw new Meteor.Error('Custom_Emoji_Error_Name_Or_Alias_Already_In_Use', 'The custom emoji or one of its aliases is already in use', {
method: 'insertOrUpdateEmoji',
});
Expand Down
9 changes: 7 additions & 2 deletions apps/meteor/server/lib/saml/lib/SAML.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,11 @@ export class SAML {
}, 5000);

try {
const loggedOutUsers = await Users.findBySAMLNameIdOrIdpSession(result.nameID, result.idpSession).toArray();
// limit 2: only need to know whether the match is missing, unique, or ambiguous
const loggedOutUsers = await Users.findBySAMLNameIdOrIdpSession(result.nameID, result.idpSession, {
projection: { _id: 1 },
limit: 2,
}).toArray();
if (loggedOutUsers.length > 1) {
throw new Meteor.Error('Found multiple users matching SAML session');
}
Expand Down Expand Up @@ -380,7 +384,8 @@ export class SAML {
const logOutUser = async (inResponseTo: string): Promise<void> => {
SAMLUtils.log({ msg: 'Processing logout for inResponseTo', inResponseTo });

const loggedOutUsers = await Users.findBySAMLInResponseTo(inResponseTo).toArray();
// limit 2: only need to know whether the match is missing, unique, or ambiguous
const loggedOutUsers = await Users.findBySAMLInResponseTo(inResponseTo, { projection: { _id: 1 }, limit: 2 }).toArray();
if (loggedOutUsers.length > 1) {
throw new Meteor.Error('Found multiple users matching SAML inResponseTo fields');
}
Expand Down
32 changes: 3 additions & 29 deletions apps/meteor/server/lib/statistics/lib/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -447,35 +447,9 @@ export const statistics = {
);

statsPms.push(
Integrations.find(
{},
{
projection: {
_id: 0,
type: 1,
enabled: 1,
scriptEnabled: 1,
},
readPreference,
},
)
.toArray()
.then((found) => {
const integrations = found;

statistics.integrations = {
totalIntegrations: integrations.length,
totalIncoming: integrations.filter((integration) => integration.type === 'webhook-incoming').length,
totalIncomingActive: integrations.filter(
(integration) => integration.enabled === true && integration.type === 'webhook-incoming',
).length,
totalOutgoing: integrations.filter((integration) => integration.type === 'webhook-outgoing').length,
totalOutgoingActive: integrations.filter(
(integration) => integration.enabled === true && integration.type === 'webhook-outgoing',
).length,
totalWithScriptEnabled: integrations.filter((integration) => integration.scriptEnabled === true).length,
};
}),
Integrations.getStatistics({ readPreference }).then((integrations) => {
statistics.integrations = integrations;
}),
);

statsPms.push(
Expand Down
3 changes: 2 additions & 1 deletion packages/model-typings/src/models/IBaseModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
ChangeStream,
ClientSession,
Collection,
CountDocumentsOptions,
DeleteOptions,
DeleteResult,
Document,
Expand Down Expand Up @@ -128,6 +129,6 @@ export interface IBaseModel<
): FindPaginated<FindCursor<WithId<TDeleted>>>;

watch(pipeline?: object[]): ChangeStream<T>;
countDocuments(query: Filter<T>): Promise<number>;
countDocuments(query: Filter<T>, options?: CountDocumentsOptions): Promise<number>;
estimatedDocumentCount(): Promise<number>;
}
1 change: 1 addition & 0 deletions packages/model-typings/src/models/ICustomSoundsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { IBaseModel } from './IBaseModel';

export interface ICustomSoundsModel extends IBaseModel<ICustomSound> {
findByName(name: string, exceptId?: string, options?: FindOptions<ICustomSound>): FindCursor<ICustomSound>;
findOneByName(name: string, exceptId?: string, options?: FindOptions<ICustomSound>): Promise<ICustomSound | null>;
create(data: Omit<ICustomSound, '_id' | '_updatedAt'>): Promise<InsertOneResult<WithId<ICustomSound>>>;
updateById(_id: string, data: Partial<Omit<ICustomSound, '_id'>>): Promise<UpdateResult>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { IBaseModel, InsertionModel } from './IBaseModel';
export interface ICustomUserStatusModel extends IBaseModel<ICustomUserStatus> {
findOneByName(name: string, options?: undefined): Promise<ICustomUserStatus | null>;
findOneByName(name: string, options?: FindOptions<ICustomUserStatus>): Promise<ICustomUserStatus | null>;
findOneByNameExceptId(name: string, except: string, options?: FindOptions<ICustomUserStatus>): Promise<ICustomUserStatus | null>;
findByName(name: string, options?: FindOptions<ICustomUserStatus>): FindCursor<ICustomUserStatus>;
findByNameExceptId(name: string, except: string, options?: FindOptions<ICustomUserStatus>): FindCursor<ICustomUserStatus>;
setName(_id: string, name: string): Promise<UpdateResult>;
Expand Down
1 change: 1 addition & 0 deletions packages/model-typings/src/models/IEmojiCustomModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { IBaseModel, InsertionModel } from './IBaseModel';

export interface IEmojiCustomModel extends IBaseModel<IEmojiCustom> {
findByNameOrAlias(emojiName: string, options?: FindOptions<IEmojiCustom>): FindCursor<IEmojiCustom>;
findOneByNamesOrAliases(names: string[], exceptId?: string, options?: FindOptions<IEmojiCustom>): Promise<IEmojiCustom | null>;
findByNameOrAliasExceptID(name: string, except: string, options?: FindOptions<IEmojiCustom>): FindCursor<IEmojiCustom>;
setName(_id: string, name: string): Promise<UpdateResult>;
setAliases(_id: string, aliases: string[]): Promise<UpdateResult>;
Expand Down
12 changes: 11 additions & 1 deletion packages/model-typings/src/models/IIntegrationsModel.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import type { IIntegration, IUser } from '@rocket.chat/core-typings';
import type { FindCursor, FindOptions } from 'mongodb';
import type { AggregateOptions, FindCursor, FindOptions } from 'mongodb';

import type { IBaseModel } from './IBaseModel';

export type IntegrationsStatistics = {
totalIntegrations: number;
totalIncoming: number;
totalIncomingActive: number;
totalOutgoing: number;
totalOutgoingActive: number;
totalWithScriptEnabled: number;
};

export interface IIntegrationsModel extends IBaseModel<IIntegration> {
disableByUserId(userId: IIntegration['userId']): ReturnType<IBaseModel<IIntegration>['updateMany']>;
findByChannels(channels: IIntegration['channel']): FindCursor<IIntegration>;
Expand All @@ -16,4 +25,5 @@ export interface IIntegrationsModel extends IBaseModel<IIntegration> {
token: string,
options?: FindOptions<P>,
): Promise<P | null>;
getStatistics(options?: AggregateOptions): Promise<IntegrationsStatistics>;
}
6 changes: 5 additions & 1 deletion packages/model-typings/src/models/ILivechatRoomsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,11 @@ export interface ILivechatRoomsModel extends IBaseModel<IOmnichannelRoom> {
association: ILivechatContactVisitorAssociation,
options?: FindOptions<IOmnichannelRoom>,
): Promise<IOmnichannelRoom | null>;
findOneOpenByVisitorToken(visitorToken: string, options?: FindOptions<IOmnichannelRoom>): Promise<IOmnichannelRoom | null>;
findOneOpenByVisitorToken(
visitorToken: string,
options?: FindOptions<IOmnichannelRoom>,
extraQuery?: Filter<IOmnichannelRoom>,
): Promise<IOmnichannelRoom | null>;
findOneOpenByVisitorTokenAndDepartmentIdAndSource(
visitorToken: string,
departmentId?: string,
Expand Down
9 changes: 9 additions & 0 deletions packages/models/src/models/CustomSounds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ export class CustomSoundsRaw extends BaseRaw<ICustomSound> implements ICustomSou
return this.find(query, options);
}

findOneByName(name: string, exceptId?: string, options?: FindOptions<ICustomSound>): Promise<ICustomSound | null> {
const query = {
name,
...(exceptId && { _id: { $nin: [exceptId] } }),
};

return this.findOne(query, options);
}

// INSERT
create(data: Omit<ICustomSound, '_id' | '_updatedAt'>): Promise<InsertOneResult<WithId<ICustomSound>>> {
return this.insertOne(data);
Expand Down
9 changes: 9 additions & 0 deletions packages/models/src/models/CustomUserStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ export class CustomUserStatusRaw extends BaseRaw<ICustomUserStatus> implements I
return options ? this.findOne({ name }, options) : this.findOne({ name });
}

findOneByNameExceptId(name: string, except: string, options?: FindOptions<ICustomUserStatus>): Promise<ICustomUserStatus | null> {
const query = {
_id: { $nin: [except] },
name,
};

return this.findOne(query, options);
}

// find
findByName(name: string, options?: FindOptions<ICustomUserStatus>): FindCursor<ICustomUserStatus> {
const query = {
Expand Down
11 changes: 10 additions & 1 deletion packages/models/src/models/EmojiCustom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IEmojiCustom, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
import type { IEmojiCustomModel, InsertionModel } from '@rocket.chat/model-typings';
import type { Collection, FindCursor, Db, FindOptions, IndexDescription, InsertOneResult, UpdateResult, WithId } from 'mongodb';
import type { Collection, Filter, FindCursor, Db, FindOptions, IndexDescription, InsertOneResult, UpdateResult, WithId } from 'mongodb';

import { BaseRaw } from './BaseRaw';

Expand Down Expand Up @@ -28,6 +28,15 @@ export class EmojiCustomRaw extends BaseRaw<IEmojiCustom> implements IEmojiCusto
return this.find(query, options);
}

findOneByNamesOrAliases(names: string[], exceptId?: string, options?: FindOptions<IEmojiCustom>): Promise<IEmojiCustom | null> {
const query: Filter<IEmojiCustom> = {
...(exceptId && { _id: { $nin: [exceptId] } }),
$or: [{ name: { $in: names } }, { aliases: { $in: names } }],
};

return this.findOne(query, options);
}

findByNameOrAliasExceptID(name: string, except: string, options?: FindOptions<IEmojiCustom>): FindCursor<IEmojiCustom> {
const query = {
_id: { $nin: [except] },
Expand Down
46 changes: 44 additions & 2 deletions packages/models/src/models/Integrations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IIntegration, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings';
import type { IBaseModel, IIntegrationsModel } from '@rocket.chat/model-typings';
import type { Collection, Db, FindCursor, FindOptions, IndexDescription } from 'mongodb';
import type { IBaseModel, IIntegrationsModel, IntegrationsStatistics } from '@rocket.chat/model-typings';
import type { AggregateOptions, Collection, Db, FindCursor, FindOptions, IndexDescription } from 'mongodb';

import { BaseRaw } from './BaseRaw';

Expand Down Expand Up @@ -72,4 +72,46 @@ export class IntegrationsRaw extends BaseRaw<IIntegration> implements IIntegrati
): Promise<P | null> {
return this.findOne<P>({ _id: id, token }, options);
}

async getStatistics(options?: AggregateOptions): Promise<IntegrationsStatistics> {
const results = await this.col
.aggregate<{ _id: IIntegration['type']; total: number; active: number; scriptEnabled: number }>(
[
{
$group: {
_id: '$type',
total: { $sum: 1 },
active: { $sum: { $cond: ['$enabled', 1, 0] } },
scriptEnabled: { $sum: { $cond: ['$scriptEnabled', 1, 0] } },
},
},
],
options,
)
.toArray();

const statistics: IntegrationsStatistics = {
totalIntegrations: 0,
totalIncoming: 0,
totalIncomingActive: 0,
totalOutgoing: 0,
totalOutgoingActive: 0,
totalWithScriptEnabled: 0,
};

for (const { _id, total, active, scriptEnabled } of results) {
statistics.totalIntegrations += total;
statistics.totalWithScriptEnabled += scriptEnabled;

if (_id === 'webhook-incoming') {
statistics.totalIncoming += total;
statistics.totalIncomingActive += active;
} else if (_id === 'webhook-outgoing') {
statistics.totalOutgoing += total;
statistics.totalOutgoingActive += active;
}
}

return statistics;
}
}
3 changes: 2 additions & 1 deletion packages/models/src/models/LivechatRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1896,11 +1896,12 @@ export class LivechatRoomsRaw extends BaseRaw<IOmnichannelRoom> implements ILive
return this.findOne(query, options);
}

findOneOpenByVisitorToken(visitorToken: string, options: FindOptions<IOmnichannelRoom> = {}) {
findOneOpenByVisitorToken(visitorToken: string, options: FindOptions<IOmnichannelRoom> = {}, extraQuery: Filter<IOmnichannelRoom> = {}) {
const query: Filter<IOmnichannelRoom> = {
't': 'l',
'open': true,
'v.token': visitorToken,
...extraQuery,
};

return this.findOne(query, options);
Expand Down
21 changes: 7 additions & 14 deletions packages/models/src/models/Messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1397,20 +1397,13 @@ export class MessagesRaw extends BaseRaw<IMessage> implements IMessagesModel {
query.tcount = { $exists: false };
}

const notCountedMessages = (
await this.find(
{
...query,
$or: [{ _hidden: true }, { editedAt: { $exists: true }, editedBy: { $exists: true }, t: 'rm' }],
},
{
projection: {
_id: 1,
},
limit,
},
).toArray()
).length;
const notCountedMessages = await this.countDocuments(
{
...query,
$or: [{ _hidden: true }, { editedAt: { $exists: true }, editedBy: { $exists: true }, t: 'rm' }],
},
{ ...(limit ? { limit } : {}) },
);

if (!limit) {
const count = (await this.deleteMany(query)).deletedCount - notCountedMessages;
Expand Down
Loading