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 2s
288 lines
8.1 KiB
JavaScript
288 lines
8.1 KiB
JavaScript
import { Team } from '@rocket.chat/core-services';
|
|
import { Users, Subscriptions as SubscriptionsRaw, Rooms } from '@rocket.chat/models';
|
|
import { escapeRegExp } from '@rocket.chat/tools';
|
|
|
|
import { canAccessRoomAsync, roomAccessAttributes } from './authorization';
|
|
import { settings } from '../settings';
|
|
import { hasPermissionAsync, hasAllPermissionAsync } from './authorization/hasPermission';
|
|
import { roomCoordinator } from './rooms/roomCoordinator';
|
|
import { trim } from '../../lib/utils/stringUtils';
|
|
import { readSecondaryPreferred } from '../database/readSecondaryPreferred';
|
|
|
|
export class Spotlight {
|
|
async fetchRooms(userId, rooms) {
|
|
if (!settings.get('Store_Last_Message') || (userId && (await hasPermissionAsync(userId, 'preview-c-room')))) {
|
|
return rooms;
|
|
}
|
|
|
|
return rooms.map((room) => {
|
|
delete room.lastMessage;
|
|
return room;
|
|
});
|
|
}
|
|
|
|
async searchRooms({ userId, text, includeFederatedRooms = false }) {
|
|
const regex = new RegExp(trim(escapeRegExp(text)), 'i');
|
|
|
|
const roomOptions = {
|
|
limit: 5,
|
|
projection: {
|
|
t: 1,
|
|
name: 1,
|
|
fname: 1,
|
|
teamMain: 1,
|
|
joinCodeRequired: 1,
|
|
lastMessage: 1,
|
|
federated: true,
|
|
prid: 1,
|
|
},
|
|
sort: {
|
|
name: 1,
|
|
},
|
|
readPreference: readSecondaryPreferred(Rooms.col.s.db),
|
|
};
|
|
|
|
if (userId == null) {
|
|
if (!settings.get('Accounts_AllowAnonymousRead')) {
|
|
return [];
|
|
}
|
|
|
|
return this.fetchRooms(userId, await Rooms.findByNameAndTypeNotDefault(regex, 'c', roomOptions, includeFederatedRooms).toArray());
|
|
}
|
|
|
|
if (!(await hasAllPermissionAsync(userId, ['view-outside-room', 'view-c-room']))) {
|
|
return [];
|
|
}
|
|
|
|
const searchableRoomTypeIds = roomCoordinator.searchableRoomTypes();
|
|
|
|
const [subscriptions, exactRoom] = await Promise.all([
|
|
SubscriptionsRaw.findByUserIdAndTypes(userId, searchableRoomTypeIds, {
|
|
projection: { rid: 1 },
|
|
readPreference: roomOptions.readPreference,
|
|
}).toArray(),
|
|
Rooms.findOneByNameAndType(text, searchableRoomTypeIds, roomOptions, includeFederatedRooms),
|
|
]);
|
|
|
|
const roomIds = subscriptions.map((s) => s.rid);
|
|
if (exactRoom) {
|
|
roomIds.push(exactRoom.rid);
|
|
}
|
|
|
|
return this.fetchRooms(
|
|
userId,
|
|
await Rooms.findByNameOrFNameAndTypesNotInIds(regex, searchableRoomTypeIds, roomIds, roomOptions, includeFederatedRooms).toArray(),
|
|
);
|
|
}
|
|
|
|
mapOutsiders(u) {
|
|
u.outside = true;
|
|
return u;
|
|
}
|
|
|
|
processLimitAndUsernames(options, usernames, users) {
|
|
// Reduce the results from the limit for the next query
|
|
options.limit -= users.length;
|
|
|
|
// If the limit was reached, return
|
|
if (options.limit <= 0) {
|
|
return users;
|
|
}
|
|
|
|
// Prevent the next query to get the same users
|
|
usernames.push(...users.map((u) => u.username).filter((u) => !usernames.includes(u)));
|
|
}
|
|
|
|
async _searchInsiderUsers({ rid, text, usernames, options, users, insiderExtraQuery, match = { startsWith: false, endsWith: false } }) {
|
|
// Get insiders first
|
|
if (rid) {
|
|
const searchFields = settings.get('Accounts_SearchFields').trim().split(',');
|
|
|
|
users.push(...(await Users.findByActiveUsersExcept(text, usernames, options, searchFields, insiderExtraQuery, match).toArray()));
|
|
|
|
// If the limit was reached, return
|
|
if (this.processLimitAndUsernames(options, usernames, users)) {
|
|
return users;
|
|
}
|
|
}
|
|
}
|
|
|
|
async _searchConnectedUsers(userId, { text, usernames, options, users, match = { startsWith: false, endsWith: false } }, roomType) {
|
|
const searchFields = settings.get('Accounts_SearchFields').trim().split(',');
|
|
|
|
users.push(
|
|
...(
|
|
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;
|
|
}
|
|
}
|
|
|
|
async _searchOutsiderUsers({ text, usernames, options, users, canListOutsiders, match = { startsWith: false, endsWith: false } }) {
|
|
// Then get the outsiders if allowed
|
|
if (canListOutsiders) {
|
|
const searchFields = settings.get('Accounts_SearchFields').trim().split(',');
|
|
users.push(
|
|
...(await Users.findByActiveUsersExcept(text, usernames, options, searchFields, undefined, match).toArray()).map(this.mapOutsiders),
|
|
);
|
|
|
|
// If the limit was reached, return
|
|
if (this.processLimitAndUsernames(options, usernames, users)) {
|
|
return users;
|
|
}
|
|
}
|
|
}
|
|
|
|
mapTeams(teams) {
|
|
return teams.map((t) => {
|
|
t.isTeam = true;
|
|
t.username = t.name;
|
|
t.status = 'online';
|
|
return t;
|
|
});
|
|
}
|
|
|
|
async _searchTeams(userId, { text, options, users, mentions }) {
|
|
if (!mentions || settings.get('Troubleshoot_Disable_Teams_Mention')) {
|
|
return users;
|
|
}
|
|
|
|
options.limit -= users.length;
|
|
|
|
if (options.limit <= 0) {
|
|
return users;
|
|
}
|
|
|
|
const teamOptions = { ...options, projection: { name: 1, type: 1 } };
|
|
const teams = await Team.search(userId, text, teamOptions);
|
|
users.push(...this.mapTeams(teams));
|
|
|
|
return users;
|
|
}
|
|
|
|
async searchUsers({ userId, rid, text, usernames, mentions }) {
|
|
const users = [];
|
|
|
|
const options = {
|
|
limit: settings.get('Number_of_users_autocomplete_suggestions'),
|
|
projection: {
|
|
username: 1,
|
|
nickname: 1,
|
|
name: 1,
|
|
status: 1,
|
|
statusText: 1,
|
|
avatarETag: 1,
|
|
},
|
|
sort: {
|
|
[settings.get('UI_Use_Real_Name') ? 'name' : 'username']: 1,
|
|
},
|
|
readPreference: readSecondaryPreferred(Users.col.s.db),
|
|
};
|
|
|
|
const room = await Rooms.findOneById(rid, { projection: { ...roomAccessAttributes, _id: 1, t: 1, uids: 1 } });
|
|
|
|
if (rid && !room) {
|
|
return users;
|
|
}
|
|
|
|
const canListOutsiders = !!userId && (await hasAllPermissionAsync(userId, ['view-outside-room', 'view-d-room']));
|
|
const canListInsiders = canListOutsiders || (rid && (await canAccessRoomAsync(room, { _id: userId })));
|
|
|
|
const insiderExtraQuery = [];
|
|
|
|
if (rid) {
|
|
switch (room.t) {
|
|
case 'd':
|
|
insiderExtraQuery.push({
|
|
_id: { $in: room.uids.filter((id) => id !== userId) },
|
|
});
|
|
break;
|
|
case 'l':
|
|
insiderExtraQuery.push({
|
|
_id: {
|
|
$in: (await SubscriptionsRaw.findByRoomId(room._id).toArray()).map((s) => s.u?._id).filter((id) => id && id !== userId),
|
|
},
|
|
});
|
|
break;
|
|
default:
|
|
insiderExtraQuery.push({
|
|
__rooms: rid,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
|
|
const searchParams = {
|
|
rid,
|
|
text,
|
|
usernames,
|
|
options,
|
|
users,
|
|
canListOutsiders,
|
|
insiderExtraQuery,
|
|
mentions,
|
|
};
|
|
|
|
// Exact match for username only
|
|
// TODO: these exact-match branches push the user without filtering against `usernames`
|
|
// (the exclusion list), so an exact username query bypasses the exclusion that the
|
|
// findByActiveUsersExcept paths below honor. Evaluate filtering exactMatch against
|
|
// `usernames` here so the exclusion applies uniformly.
|
|
if (rid && canListInsiders) {
|
|
const exactMatch = await Users.findOneByUsernameAndRoomIgnoringCase(text, rid, {
|
|
projection: options.projection,
|
|
readPreference: options.readPreference,
|
|
});
|
|
if (exactMatch) {
|
|
users.push(exactMatch);
|
|
this.processLimitAndUsernames(options, usernames, users);
|
|
}
|
|
}
|
|
|
|
if (users.length === 0 && canListOutsiders && text) {
|
|
const exactMatch = await Users.findOneByUsernameIgnoringCase(text, {
|
|
projection: options.projection,
|
|
readPreference: options.readPreference,
|
|
});
|
|
if (exactMatch) {
|
|
users.push(this.mapOutsiders(exactMatch));
|
|
this.processLimitAndUsernames(options, usernames, users);
|
|
}
|
|
}
|
|
|
|
if (canListInsiders && rid) {
|
|
// Search for insiders
|
|
if (await this._searchInsiderUsers(searchParams)) {
|
|
return users;
|
|
}
|
|
|
|
// Search for users that the requester has DMs with
|
|
if (await this._searchConnectedUsers(userId, searchParams, 'd')) {
|
|
return users;
|
|
}
|
|
}
|
|
|
|
// 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 (await this._searchOutsiderUsers(searchParams)) {
|
|
return users;
|
|
}
|
|
} else if (await this._searchConnectedUsers(userId, searchParams, 'd')) {
|
|
return users;
|
|
}
|
|
|
|
if (await this._searchTeams(userId, searchParams)) {
|
|
return users;
|
|
}
|
|
|
|
return users;
|
|
}
|
|
}
|