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:
@@ -0,0 +1,58 @@
|
||||
import type { IServiceClass } from '@rocket.chat/core-services';
|
||||
import { Settings, StatusVisibility } from '@rocket.chat/core-services';
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
|
||||
export const STATUS_VISIBILITY_SETTING_ID = 'Accounts_StatusVisibility_Enabled';
|
||||
|
||||
export class StatusVisibilityGate {
|
||||
private enabledLookup?: Promise<boolean>;
|
||||
|
||||
private restricted?: Set<IUser['_id']>;
|
||||
|
||||
private pendingSync?: Promise<void>;
|
||||
|
||||
watch(service: IServiceClass): void {
|
||||
service.onSettingChanged(STATUS_VISIBILITY_SETTING_ID, async ({ setting }) => {
|
||||
this.enabledLookup = Promise.resolve(setting.value === true);
|
||||
});
|
||||
}
|
||||
|
||||
ensureEnabled(): Promise<boolean> {
|
||||
if (!this.enabledLookup) {
|
||||
this.enabledLookup = Settings.get<boolean>(STATUS_VISIBILITY_SETTING_ID)
|
||||
.then((value) => value === true)
|
||||
.catch(() => {
|
||||
this.enabledLookup = undefined;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return this.enabledLookup;
|
||||
}
|
||||
|
||||
hasRestrictions(targetId: IUser['_id']): boolean {
|
||||
if (!this.restricted) {
|
||||
void this.syncRestrictedUsers();
|
||||
return true;
|
||||
}
|
||||
|
||||
return this.restricted.has(targetId);
|
||||
}
|
||||
|
||||
syncRestrictedUsers(): Promise<void> {
|
||||
if (!this.pendingSync) {
|
||||
this.pendingSync = StatusVisibility.getRestrictedUsers()
|
||||
.then((users) => {
|
||||
this.restricted = new Set(users);
|
||||
})
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
this.pendingSync = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
return this.pendingSync;
|
||||
}
|
||||
}
|
||||
|
||||
export const statusVisibilityGate = new StatusVisibilityGate();
|
||||
@@ -0,0 +1,40 @@
|
||||
import { UserStatus } from '@rocket.chat/core-typings';
|
||||
import { expect } from 'chai';
|
||||
|
||||
import { effectiveStatusExpression, effectiveStatusFilter, excludingOfflineFilter } from './effectiveStatus';
|
||||
|
||||
describe('effectiveStatusFilter', () => {
|
||||
it('should filter by the stored status when nobody is hidden', () => {
|
||||
expect(effectiveStatusFilter([UserStatus.ONLINE])).to.be.deep.equal({ status: { $in: [UserStatus.ONLINE] } });
|
||||
expect(effectiveStatusFilter([UserStatus.ONLINE], new Set())).to.be.deep.equal({ status: { $in: [UserStatus.ONLINE] } });
|
||||
});
|
||||
|
||||
it('should exclude hidden users from a filter that does not include offline', () => {
|
||||
expect(effectiveStatusFilter([UserStatus.ONLINE, UserStatus.BUSY], new Set(['alice']))).to.be.deep.equal({
|
||||
$and: [{ _id: { $nin: ['alice'] } }, { status: { $in: [UserStatus.ONLINE, UserStatus.BUSY] } }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should include every hidden user in a filter that includes offline, whatever their stored status', () => {
|
||||
expect(effectiveStatusFilter([UserStatus.ONLINE, UserStatus.OFFLINE], new Set(['alice']))).to.be.deep.equal({
|
||||
$or: [{ _id: { $nin: ['alice'] }, status: { $in: [UserStatus.ONLINE, UserStatus.OFFLINE] } }, { _id: { $in: ['alice'] } }],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('effectiveStatusExpression', () => {
|
||||
it('should resolve hidden users to offline and leave everyone else untouched', () => {
|
||||
expect(effectiveStatusExpression(new Set(['alice', 'bob']))).to.be.deep.equal({
|
||||
$cond: [{ $in: ['$_id', ['alice', 'bob']] }, UserStatus.OFFLINE, '$status'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('excludingOfflineFilter', () => {
|
||||
it('should leave hidden users out, whatever their stored status', () => {
|
||||
expect(excludingOfflineFilter()).to.be.deep.equal({ status: { $ne: UserStatus.OFFLINE } });
|
||||
expect(excludingOfflineFilter(new Set(['alice']))).to.be.deep.equal({
|
||||
$and: [{ _id: { $nin: ['alice'] } }, { status: { $ne: UserStatus.OFFLINE } }],
|
||||
});
|
||||
});
|
||||
});
|
||||
26
apps/meteor/server/lib/statusVisibility/effectiveStatus.ts
Normal file
26
apps/meteor/server/lib/statusVisibility/effectiveStatus.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import { UserStatus } from '@rocket.chat/core-typings';
|
||||
import type { Filter } from 'mongodb';
|
||||
|
||||
export const effectiveStatusFilter = (status: UserStatus[], hidden?: Set<IUser['_id']>): Filter<IUser> => {
|
||||
if (!hidden?.size) {
|
||||
return { status: { $in: status } };
|
||||
}
|
||||
|
||||
const ids = [...hidden];
|
||||
|
||||
if (status.includes(UserStatus.OFFLINE)) {
|
||||
return { $or: [{ _id: { $nin: ids }, status: { $in: status } }, { _id: { $in: ids } }] };
|
||||
}
|
||||
|
||||
return { $and: [{ _id: { $nin: ids } }, { status: { $in: status } }] };
|
||||
};
|
||||
|
||||
export const effectiveStatusExpression = (hidden: Set<IUser['_id']>) => ({
|
||||
$cond: [{ $in: ['$_id', [...hidden]] }, UserStatus.OFFLINE, '$status'],
|
||||
});
|
||||
|
||||
export const excludingOfflineFilter = (hidden?: Set<IUser['_id']>): Filter<IUser> =>
|
||||
hidden?.size
|
||||
? { $and: [{ _id: { $nin: [...hidden] } }, { status: { $ne: UserStatus.OFFLINE } }] }
|
||||
: { status: { $ne: UserStatus.OFFLINE } };
|
||||
21
apps/meteor/server/lib/statusVisibility/hiddenUsers.ts
Normal file
21
apps/meteor/server/lib/statusVisibility/hiddenUsers.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { StatusVisibility } from '@rocket.chat/core-services';
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
|
||||
import { redactStatus } from './redactStatus';
|
||||
import { settings } from '../../settings';
|
||||
|
||||
export const getUsersHiddenFrom = async (viewerId: IUser['_id'] | null | undefined): Promise<Set<IUser['_id']> | undefined> => {
|
||||
if (!settings.get<boolean>('Accounts_StatusVisibility_Enabled')) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const hidden = await StatusVisibility.getHiddenFrom(viewerId);
|
||||
|
||||
return hidden.length ? new Set(hidden) : undefined;
|
||||
};
|
||||
|
||||
export const filterHiddenUsers = <T extends Pick<IUser, '_id'>>(users: T[], hidden?: Set<IUser['_id']>): T[] =>
|
||||
hidden ? users.filter((user) => !hidden.has(user._id)) : users;
|
||||
|
||||
export const redactHiddenUsers = <T extends Pick<IUser, '_id'>>(users: T[], hidden?: Set<IUser['_id']>): T[] =>
|
||||
hidden ? users.map((user) => (hidden.has(user._id) ? redactStatus(user) : user)) : users;
|
||||
21
apps/meteor/server/lib/statusVisibility/redactStatus.ts
Normal file
21
apps/meteor/server/lib/statusVisibility/redactStatus.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
|
||||
type WithPresenceFields = Partial<Pick<IUser, 'statusText' | 'statusSource' | 'statusExpiresAt' | 'statusDefault' | 'statusConnection'>> & {
|
||||
status?: string;
|
||||
};
|
||||
|
||||
export const redactStatus = <T extends WithPresenceFields>(user: T): T => {
|
||||
const { statusText, statusSource, statusExpiresAt, statusDefault, statusConnection, ...rest } = user;
|
||||
|
||||
return { ...rest, status: 'offline' } as T;
|
||||
};
|
||||
|
||||
export const omitStatusVisibilityConfig = (userSettings: IUser['settings']): IUser['settings'] => {
|
||||
if (!userSettings?.preferences || !('statusVisibilityDenied' in userSettings.preferences)) {
|
||||
return userSettings;
|
||||
}
|
||||
|
||||
const { statusVisibilityDenied: _denied, ...preferences } = userSettings.preferences;
|
||||
|
||||
return { ...userSettings, preferences };
|
||||
};
|
||||
46
apps/meteor/server/lib/statusVisibility/resolveUsers.spec.ts
Normal file
46
apps/meteor/server/lib/statusVisibility/resolveUsers.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { resolveUsersByIds, resolveUsersByUsernames } from './resolveUsers';
|
||||
|
||||
const usersByUsernames = jest.fn();
|
||||
const usersByIds = jest.fn();
|
||||
|
||||
jest.mock('@rocket.chat/models', () => ({
|
||||
Users: {
|
||||
findByUsernames: (...args: unknown[]) => usersByUsernames(...args),
|
||||
findByIds: (...args: unknown[]) => usersByIds(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('status visibility user resolution', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it('pairs every resolved id with its username in either direction', async () => {
|
||||
usersByUsernames.mockReturnValue([
|
||||
{ _id: 'bbb222', username: 'bruno' },
|
||||
{ _id: 'ccc333', username: 'carla' },
|
||||
]);
|
||||
|
||||
const byUsername = await resolveUsersByUsernames(['bruno', 'ghost', 'carla']);
|
||||
|
||||
expect(byUsername.usernames).toEqual(['bruno', 'carla']);
|
||||
expect(byUsername.ids).toEqual(['bbb222', 'ccc333']);
|
||||
|
||||
usersByIds.mockReturnValue([{ _id: 'bbb222', username: 'bruno' }]);
|
||||
|
||||
const byId = await resolveUsersByIds(['bbb222', 'missing']);
|
||||
|
||||
expect(byId.usernames).toEqual(['bruno']);
|
||||
expect(byId.ids).toEqual(['bbb222']);
|
||||
});
|
||||
|
||||
it('resolves to empty arrays without touching the database', async () => {
|
||||
const { ids, usernames } = await resolveUsersByUsernames([]);
|
||||
|
||||
expect(ids).toEqual([]);
|
||||
expect(usernames).toEqual([]);
|
||||
expect(usersByUsernames).not.toHaveBeenCalled();
|
||||
expect((await resolveUsersByIds([])).ids).toEqual([]);
|
||||
expect(usersByIds).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
34
apps/meteor/server/lib/statusVisibility/resolveUsers.ts
Normal file
34
apps/meteor/server/lib/statusVisibility/resolveUsers.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import { Users } from '@rocket.chat/models';
|
||||
import type { FindCursor } from 'mongodb';
|
||||
|
||||
type ResolvedUsers = { ids: IUser['_id'][]; usernames: NonNullable<IUser['username']>[] };
|
||||
|
||||
const collectUsers = async (users: FindCursor<Pick<IUser, '_id' | 'username'>>): Promise<ResolvedUsers> => {
|
||||
const resolved: ResolvedUsers = { ids: [], usernames: [] };
|
||||
|
||||
for await (const { _id, username } of users) {
|
||||
if (username) {
|
||||
resolved.ids.push(_id);
|
||||
resolved.usernames.push(username);
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
};
|
||||
|
||||
export const resolveUsersByIds = async (ids: IUser['_id'][]): Promise<ResolvedUsers> => {
|
||||
if (!ids.length) {
|
||||
return { ids: [], usernames: [] };
|
||||
}
|
||||
|
||||
return collectUsers(Users.findByIds<Pick<IUser, '_id' | 'username'>>(ids, { projection: { username: 1 } }));
|
||||
};
|
||||
|
||||
export const resolveUsersByUsernames = async (usernames: string[]): Promise<ResolvedUsers> => {
|
||||
if (!usernames.length) {
|
||||
return { ids: [], usernames: [] };
|
||||
}
|
||||
|
||||
return collectUsers(Users.findByUsernames(usernames, { projection: { username: 1 } }));
|
||||
};
|
||||
Reference in New Issue
Block a user