Skip to content

Commit

Permalink
[IMPROVE] Spotlight search user results (RocketChat#26599)
Browse files Browse the repository at this point in the history
  • Loading branch information
pierre-lehnen-rc authored Aug 24, 2022
1 parent 72de81b commit 7ada86f
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 16 deletions.
49 changes: 37 additions & 12 deletions apps/meteor/server/lib/spotlight.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import s from 'underscore.string';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Users } from '@rocket.chat/models';
import { Users, Subscriptions as SubscriptionsRaw } from '@rocket.chat/models';

import { hasAllPermission, hasPermission, canAccessRoom, roomAccessAttributes } from '../../app/authorization/server';
import { Subscriptions, Rooms } from '../../app/models/server';
Expand Down Expand Up @@ -97,6 +97,24 @@ export class Spotlight {
}
}

_searchConnectedUsers(userId, { text, usernames, options, users, match = { startsWith: false, endsWith: false } }, roomType) {
const searchFields = settings.get('Accounts_SearchFields').trim().split(',');

users.push(
...Promise.await(
SubscriptionsRaw.findConnectedUsersExcept(userId, text, usernames, searchFields, options.limit || 5, roomType, match),
{
readPreference: options.readPreference,
},
).map(this.mapOutsiders),
);

// If the limit was reached, return
if (this.processLimitAndUsernames(options, usernames, users)) {
return users;
}
}

_searchOutsiderUsers({ text, usernames, options, users, canListOutsiders, match = { startsWith: false, endsWith: false } }) {
// Then get the outsiders if allowed
if (canListOutsiders) {
Expand Down Expand Up @@ -146,11 +164,6 @@ export class Spotlight {
const canListOutsiders = hasAllPermission(userId, ['view-outside-room', 'view-d-room']);
const canListInsiders = canListOutsiders || (rid && canAccessRoom(room, { _id: userId }));

// If can't list outsiders and, wether, the rid was not passed or the user has no access to the room, return
if (!canListOutsiders && !canListInsiders) {
return users;
}

const insiderExtraQuery = [];

if (rid) {
Expand Down Expand Up @@ -190,7 +203,7 @@ export class Spotlight {
};

// Exact match for username only
if (rid) {
if (rid && canListInsiders) {
const exactMatch = Promise.await(
Users.findOneByUsernameAndRoomIgnoringCase(text, rid, {
projection: options.projection,
Expand All @@ -216,13 +229,25 @@ export class Spotlight {
}
}

// Contains for insiders
if (this._searchInsiderUsers(searchParams)) {
return users;
if (canListInsiders && rid) {
// Search for insiders
if (this._searchInsiderUsers(searchParams)) {
return users;
}

// Search for users that the requester has DMs with
if (this._searchConnectedUsers(userId, searchParams, 'd')) {
return users;
}
}

// Contains for outsiders
if (this._searchOutsiderUsers(searchParams)) {
// If the user can search outsiders, search for any user in the server
// Otherwise, search for users that are subscribed to the same rooms as the requester
if (canListOutsiders) {
if (this._searchOutsiderUsers(searchParams)) {
return users;
}
} else if (this._searchConnectedUsers(userId, searchParams)) {
return users;
}

Expand Down
121 changes: 119 additions & 2 deletions apps/meteor/server/models/raw/Subscriptions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IRole, IRoom, ISubscription, IUser, RocketChatRecordDeleted, RoomType } from '@rocket.chat/core-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { IRole, IRoom, ISubscription, IUser, RocketChatRecordDeleted, RoomType, SpotlightUser } from '@rocket.chat/core-typings';
import type { ISubscriptionsModel } from '@rocket.chat/model-typings';
import type { Collection, FindCursor, Db, Filter, FindOptions, UpdateResult, Document } from 'mongodb';
import type { Collection, FindCursor, Db, Filter, FindOptions, UpdateResult, Document, AggregateOptions } from 'mongodb';
import { Users } from '@rocket.chat/models';
import { compact } from 'lodash';

Expand Down Expand Up @@ -234,6 +235,122 @@ export class SubscriptionsRaw extends BaseRaw<ISubscription> implements ISubscri
return this.find(query, options || {});
}

async findConnectedUsersExcept(
userId: string,
searchTerm: string,
exceptions: string[],
searchFields: string[],
limit: number,
roomType?: ISubscription['t'],
{ startsWith = false, endsWith = false }: { startsWith?: string | false; endsWith?: string | false } = {},
options: AggregateOptions = {},
): Promise<SpotlightUser[]> {
const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i');
const orStatement = searchFields.reduce(function (acc, el) {
acc.push({ [el.trim()]: termRegex });
return acc;
}, [] as { [x: string]: RegExp }[]);

return this.col
.aggregate<SpotlightUser>(
[
// Match all subscriptions of the requester
{
$match: {
'u._id': userId,
...(roomType ? { t: roomType } : {}),
},
},
// Group by room id and drop all other subcription data
{
$group: {
_id: '$rid',
},
},
// find all subscriptions to the same rooms by other users
{
$lookup: {
from: 'rocketchat_subscription',
as: 'subscription',
let: {
rid: '$_id',
},
pipeline: [{ $match: { '$expr': { $eq: ['$rid', '$$rid'] }, 'u._id': { $ne: userId } } }],
},
},
// Unwind the subscription so we have a separate document for each
{
$unwind: {
path: '$subscription',
},
},
// Group the data by user id, keeping track of how many documents each user had
{
$group: {
_id: '$subscription.u._id',
score: {
$sum: 1,
},
},
},
// Load the data for the subscription's user, ignoring those who don't match the search terms
{
$lookup: {
from: 'users',
as: 'user',
let: { id: '$_id' },
pipeline: [
{
$match: {
$expr: { $eq: ['$_id', '$$id'] },
active: true,
username: {
$exists: true,
...(exceptions.length > 0 && { $nin: exceptions }),
},
...(searchTerm && orStatement.length > 0 && { $or: orStatement }),
},
},
],
},
},
// Discard documents that didn't load any user data in the previous step:
{
$unwind: {
path: '$user',
},
},
// Use group to organize the data at the same time that we pick what to project to the end result
{
$group: {
_id: '$_id',
score: {
$sum: '$score',
},
name: { $first: '$user.name' },
username: { $first: '$user.username' },
nickname: { $first: '$user.nickname' },
status: { $first: '$user.status' },
statusText: { $first: '$user.statusText' },
avatarETag: { $first: '$user.avatarETag' },
},
},
// Sort by score
{
$sort: {
score: -1,
},
},
// Limit the number of results
{
$limit: limit,
},
],
options,
)
.toArray();
}

incUnreadForRoomIdExcludingUserIds(roomId: IRoom['_id'], userIds: IUser['_id'][], inc: number): Promise<UpdateResult | Document> {
if (inc == null) {
inc = 1;
Expand Down
11 changes: 11 additions & 0 deletions packages/core-typings/src/SpotlightUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { IUser } from './IUser';

export type SpotlightUser = {
_id: IUser['_id'];
username: Required<IUser>['username'];
nickname: IUser['nickname'];
name: IUser['name'];
status: IUser['status'];
statusText: IUser['statusText'];
avatarETag: IUser['avatarETag'];
};
2 changes: 2 additions & 0 deletions packages/core-typings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,5 @@ export * from './IAutoTranslate';
export * from './IVideoConference';
export * from './VideoConferenceCapabilities';
export * from './VideoConferenceOptions';

export * from './SpotlightUser';
15 changes: 13 additions & 2 deletions packages/model-typings/src/models/ISubscriptionsModel.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { FindOptions, FindCursor, UpdateResult, Document } from 'mongodb';
import type { ISubscription, IRole, IUser, IRoom, RoomType } from '@rocket.chat/core-typings';
import type { FindOptions, FindCursor, UpdateResult, Document, AggregateOptions } from 'mongodb';
import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser } from '@rocket.chat/core-typings';

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

Expand Down Expand Up @@ -57,6 +57,17 @@ export interface ISubscriptionsModel extends IBaseModel<ISubscription> {

findByUserIdAndTypes(userId: string, types: ISubscription['t'][], options?: FindOptions<ISubscription>): FindCursor<ISubscription>;

findConnectedUsersExcept(
userId: string,
searchTerm: string,
exceptions: string[],
searchFields: string[],
limit: number,
roomType?: ISubscription['t'],
{ startsWith, endsWith }?: { startsWith?: string | false; endsWith?: string | false },
options?: AggregateOptions,
): Promise<SpotlightUser[]>;

incUnreadForRoomIdExcludingUserIds(roomId: IRoom['_id'], userIds: IUser['_id'][], inc: number): Promise<UpdateResult | Document>;

setAlertForRoomIdExcludingUserId(roomId: IRoom['_id'], userId: IUser['_id']): Promise<UpdateResult | Document>;
Expand Down

0 comments on commit 7ada86f

Please sign in to comment.