Skip to content

Commit

Permalink
[BREAK][ENTERPRISE] Limit presence statuses to 200 concurrent users w…
Browse files Browse the repository at this point in the history
…hen running monolith to keep performance (#27854)

Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
Co-authored-by: Júlia Jaeger Foresti <60678893+juliajforesti@users.noreply.github.com>
Co-authored-by: Rodrigo Nascimento <rodrigoknascimento@gmail.com>
  • Loading branch information
4 people authored Feb 10, 2023
1 parent 58898f0 commit 0d9b086
Show file tree
Hide file tree
Showing 58 changed files with 938 additions and 250 deletions.
1 change: 0 additions & 1 deletion apps/meteor/.meteor/packages
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ rocketchat:livechat
rocketchat:streamer
rocketchat:version

konecty:multiple-instances-status
konecty:user-presence

dispatch:run-as-user
Expand Down
1 change: 0 additions & 1 deletion apps/meteor/.meteor/versions
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ jparker:crypto-md5@0.1.1
jparker:gravatar@0.5.1
jquery@3.0.0
kadira:flow-router@2.12.1
konecty:multiple-instances-status@1.1.0
konecty:user-presence@2.6.3
launch-screen@1.3.0
littledata:synced-cron@1.5.1
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/api/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import './v1/import';
import './v1/ldap';
import './v1/misc';
import './v1/permissions';
import './v1/presence';
import './v1/push';
import './v1/roles';
import './v1/rooms.js';
Expand Down
27 changes: 27 additions & 0 deletions apps/meteor/app/api/server/v1/presence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Presence } from '@rocket.chat/core-services';

import { API } from '../api';

API.v1.addRoute(
'presence.getConnections',
{ authRequired: true, permissionsRequired: ['manage-user-status'] },
{
async get() {
const result = await Presence.getConnectionCount();

return API.v1.success(result);
},
},
);

API.v1.addRoute(
'presence.enableBroadcast',
{ authRequired: true, permissionsRequired: ['manage-user-status'], twoFactorRequired: true },
{
async post() {
await Presence.toggleBroadcast(true);

return API.v1.success();
},
},
);
8 changes: 8 additions & 0 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -775,6 +775,14 @@ API.v1.addRoute(
{ authRequired: true },
{
get() {
// if presence broadcast is disabled, return an empty array (all users are "offline")
if (settings.get('Presence_broadcast_disabled')) {
return API.v1.success({
users: [],
full: true,
});
}

const { from, ids } = this.queryParams;

const options = {
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/apps/server/storage/logs-storage.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AppConsole } from '@rocket.chat/apps-engine/server/logging';
import { AppLogStorage } from '@rocket.chat/apps-engine/server/storage';
import { InstanceStatus } from 'meteor/konecty:multiple-instances-status';
import { InstanceStatus } from '@rocket.chat/instance-status';

export class AppRealLogsStorage extends AppLogStorage {
constructor(model) {
Expand Down
5 changes: 3 additions & 2 deletions apps/meteor/app/file-upload/server/lib/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import URL from 'url';
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { UploadFS } from 'meteor/jalik:ufs';
import { InstanceStatus } from 'meteor/konecty:multiple-instances-status';
import { InstanceStatus } from '@rocket.chat/instance-status';
import { InstanceStatus as InstanceStatusModel } from '@rocket.chat/models';

import { Logger } from '../../../logger';
import { isDocker } from '../../../utils';
Expand Down Expand Up @@ -63,7 +64,7 @@ WebApp.connectHandlers.stack.unshift({
}

// Proxy to other instance
const instance = InstanceStatus.getCollection().findOne({ _id: file.instanceId });
const instance = Promise.await(InstanceStatusModel.findOneById(file.instanceId));

if (instance == null) {
res.writeHead(404);
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/lib/server/lib/debug.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { WebApp } from 'meteor/webapp';
import { InstanceStatus } from 'meteor/konecty:multiple-instances-status';
import { InstanceStatus } from '@rocket.chat/instance-status';
import _ from 'underscore';

import { settings } from '../../../settings/server';
Expand Down
10 changes: 10 additions & 0 deletions apps/meteor/app/lib/server/startup/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3186,10 +3186,20 @@ settingsRegistry.addGroup('Troubleshoot', function () {
type: 'boolean',
alert: 'Troubleshoot_Disable_Notifications_Alert',
});

// this settings will let clients know in case presence has been disabled
this.add('Presence_broadcast_disabled', false, {
type: 'boolean',
public: true,
blocked: true,
});

this.add('Troubleshoot_Disable_Presence_Broadcast', false, {
type: 'boolean',
alert: 'Troubleshoot_Disable_Presence_Broadcast_Alert',
enableQuery: { _id: 'Presence_broadcast_disabled', value: false },
});

this.add('Troubleshoot_Disable_Instance_Broadcast', false, {
type: 'boolean',
alert: 'Troubleshoot_Disable_Instance_Broadcast_Alert',
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/notifications/client/lib/Presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor';
import { Presence, STATUS_MAP } from '../../../../client/lib/presence';

// TODO implement API on Streamer to be able to listen to all streamed data
// this is a hacky way to listen to all streamed data from user-presense Streamer
// this is a hacky way to listen to all streamed data from user-presence Streamer
(Meteor as any).StreamerCentral.on('stream-user-presence', (uid: string, args: unknown) => {
if (!Array.isArray(args)) {
throw new Error('Presence event must be an array');
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/client/hooks/usePresence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { UserPresence } from '../lib/presence';
import { Presence } from '../lib/presence';

/**
* @deprecated
* Hook to fetch and subscribe users presence
*
* @param uid - User Id
Expand Down
13 changes: 0 additions & 13 deletions apps/meteor/client/hooks/useUserData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,6 @@ import type { UserPresence } from '../lib/presence';
*/
export const useUserData = (uid: string): UserPresence | undefined => {
const userPresence = useContext(UserPresenceContext);
// const subscription = useCallback(
// (callback: () => void): (() => void) => {
// Presence.listen(uid, callback);
// return (): void => {
// Presence.stop(uid, callback);
// };
// },
// [uid],
// );

// const getSnapshot = (): UserPresence | undefined => Presence.store.get(uid);

// return useSyncExternalStore(subscription, getSnapshot);

const { subscribe, get } = useMemo(
() => userPresence?.queryUserData(uid) ?? { subscribe: () => () => undefined, get: () => undefined },
Expand Down
13 changes: 11 additions & 2 deletions apps/meteor/client/lib/presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor';

import { APIClient } from '../../app/utils/client';

export const STATUS_MAP = [UserStatus.OFFLINE, UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY];
export const STATUS_MAP = [UserStatus.OFFLINE, UserStatus.ONLINE, UserStatus.AWAY, UserStatus.BUSY, UserStatus.DISABLED];

type InternalEvents = {
remove: IUser['_id'];
Expand Down Expand Up @@ -35,7 +35,7 @@ const uids = new Set<UserPresence['_id']>();

const update: EventHandlerOf<ExternalEvents, string> = (update) => {
if (update?._id) {
store.set(update._id, { ...store.get(update._id), ...update });
store.set(update._id, { ...store.get(update._id), ...update, ...(status === 'disabled' && { status: UserStatus.DISABLED }) });
uids.delete(update._id);
}
};
Expand Down Expand Up @@ -175,7 +175,16 @@ const get = async (uid: UserPresence['_id']): Promise<UserPresence | undefined>
listen(uid, callback);
});

let status = 'enabled';

const setStatus = (newStatus: 'enabled' | 'disabled'): void => {
status = newStatus;
reset();
};

export const Presence = {
setStatus,
status,
listen,
stop,
reset,
Expand Down
9 changes: 8 additions & 1 deletion apps/meteor/client/providers/UserPresenceProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useSetting } from '@rocket.chat/ui-contexts';
import type { ReactElement, ReactNode } from 'react';
import React, { useMemo } from 'react';
import React, { useMemo, useEffect } from 'react';

import { UserPresenceContext } from '../contexts/UserPresenceContext';
import { Presence } from '../lib/presence';
Expand All @@ -9,6 +10,12 @@ type UserPresenceProviderProps = {
};

const UserPresenceProvider = ({ children }: UserPresenceProviderProps): ReactElement => {
const usePresenceDisabled = useSetting<boolean>('Presence_broadcast_disabled');

useEffect(() => {
Presence.setStatus(usePresenceDisabled ? 'disabled' : 'enabled');
}, [usePresenceDisabled]);

return (
<UserPresenceContext.Provider
value={useMemo(
Expand Down
7 changes: 6 additions & 1 deletion apps/meteor/client/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { css } from '@rocket.chat/css-in-js';
import { Box, Palette } from '@rocket.chat/fuselage';
import { useLayout, useUserPreference } from '@rocket.chat/ui-contexts';
import { useSessionStorage } from '@rocket.chat/fuselage-hooks';
import { useLayout, useSetting, useUserPreference } from '@rocket.chat/ui-contexts';
import React from 'react';

import SidebarRoomList from './RoomList';
import SidebarFooter from './footer';
import SidebarHeader from './header';
import StatusDisabledSection from './sections/StatusDisabledSection';

const Sidebar = () => {
const sidebarViewMode = useUserPreference('sidebarViewMode');
const sidebarHideAvatar = !useUserPreference('sidebarDisplayAvatar');
const { isMobile, sidebar } = useLayout();
const [bannerDismissed, setBannerDismissed] = useSessionStorage('presence_cap_notifier', false);
const presenceDisabled = useSetting<boolean>('Presence_broadcast_disabled');

const sideBarBackground = css`
background-color: ${Palette.surface['surface-tint']};
Expand Down Expand Up @@ -98,6 +102,7 @@ const Sidebar = () => {
data-qa-opened={sidebar.isCollapsed ? 'false' : 'true'}
>
<SidebarHeader />
{presenceDisabled && !bannerDismissed && <StatusDisabledSection onDismiss={() => setBannerDismissed(true)} />}
<SidebarRoomList />
<SidebarFooter />
</Box>
Expand Down
46 changes: 42 additions & 4 deletions apps/meteor/client/sidebar/header/UserDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ import {
RadioButton,
} from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useLayout, useRoute, useLogout, useSetting, useTranslation } from '@rocket.chat/ui-contexts';
import type { TranslationKey } from '@rocket.chat/ui-contexts';
import { useLayout, useRoute, useLogout, useSetting, useTranslation, useSetModal } from '@rocket.chat/ui-contexts';
import { useThemeMode } from '@rocket.chat/ui-theming/src/hooks/useThemeMode';
import type { ReactElement } from 'react';
import React from 'react';

import { AccountBox } from '../../../app/ui-utils/client';
import { userStatus } from '../../../app/user-status/client';
import { callbacks } from '../../../lib/callbacks';
import GenericModal from '../../components/GenericModal';
import MarkdownText from '../../components/MarkdownText';
import { UserStatus } from '../../components/UserStatus';
import UserAvatar from '../../components/avatar/UserAvatar';
Expand All @@ -38,7 +40,7 @@ const setStatus = (status: typeof userStatus.list['']): void => {

const translateStatusName = (t: ReturnType<typeof useTranslation>, status: typeof userStatus.list['']): string => {
if (isDefaultStatusName(status.name, status.id)) {
return t(status.name);
return t(status.name as TranslationKey);
}

return status.name;
Expand All @@ -52,9 +54,18 @@ type UserDropdownProps = {
const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => {
const t = useTranslation();
const accountRoute = useRoute('account-index');
const userStatusRoute = useRoute('user-status');
const logout = useLogout();
const { isMobile } = useLayout();
const presenceDisabled = useSetting<boolean>('Presence_broadcast_disabled');

const setModal = useSetModal();
const closeModal = useMutableCallback(() => setModal());
const handleGoToSettings = useMutableCallback(() => {
userStatusRoute.push({});
closeModal();
onClose();
});
const [selectedTheme, setTheme] = useThemeMode();

const { username, avatarETag, status, statusText } = user;
Expand Down Expand Up @@ -103,14 +114,40 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => {
<MarkdownText
withTruncatedText
parseEmoji={true}
content={statusText || t(status || 'offline')}
content={statusText || t(status ?? 'offline')}
variant='inlineWithoutBreaks'
/>
</Box>
</Box>
</Box>
<OptionDivider />
<OptionTitle>{t('Status')}</OptionTitle>
{presenceDisabled && (
<Box fontScale='p2' mi='x12' mb='x4'>
<Box mbe='x4'>{t('User_status_disabled')}</Box>
<Box
is='a'
color='status-font-on-info'
onClick={() =>
setModal(
<GenericModal
title={t('User_status_disabled_learn_more')}
cancelText={t('Close')}
confirmText={t('Go_to_workspace_settings')}
children={t('User_status_disabled_learn_more_description')}
onConfirm={handleGoToSettings}
onClose={closeModal}
onCancel={closeModal}
icon={null}
variant='warning'
/>,
)
}
>
{t('Learn_more')}
</Box>
</Box>
)}
{Object.values(userStatus.list)
.filter(filterInvisibleStatus)
.map((status, i) => {
Expand All @@ -120,6 +157,7 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => {
return (
<Option
key={i}
disabled={presenceDisabled}
onClick={(): void => {
setStatus(status);
onClose();
Expand All @@ -134,7 +172,7 @@ const UserDropdown = ({ user, onClose }: UserDropdownProps): ReactElement => {
</Option>
);
})}
<Option icon='emoji' label={`${t('Custom_Status')}...`} onClick={handleCustomStatus}></Option>
<Option icon='emoji' label={`${t('Custom_Status')}...`} onClick={handleCustomStatus} disabled={presenceDisabled}></Option>
<OptionDivider />

<OptionTitle>{t('Theme')}</OptionTitle>
Expand Down
42 changes: 42 additions & 0 deletions apps/meteor/client/sidebar/sections/StatusDisabledSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { SidebarBanner } from '@rocket.chat/fuselage';
import { useMutableCallback } from '@rocket.chat/fuselage-hooks';
import { useRoute, useSetModal, useTranslation } from '@rocket.chat/ui-contexts';
import React from 'react';

import GenericModal from '../../components/GenericModal';

const StatusDisabledSection = ({ onDismiss }: { onDismiss: () => void }) => {
const t = useTranslation();
const userStatusRoute = useRoute('user-status');
const setModal = useSetModal();
const closeModal = useMutableCallback(() => setModal());
const handleGoToSettings = useMutableCallback(() => {
userStatusRoute.push({});
closeModal();
});

return (
<SidebarBanner
text={t('User_status_temporarily_disabled')}
description={t('Learn_more')}
onClose={onDismiss}
onClick={() =>
setModal(
<GenericModal
title={t('User_status_disabled_learn_more')}
cancelText={t('Close')}
confirmText={t('Go_to_workspace_settings')}
children={t('User_status_disabled_learn_more_description')}
onConfirm={handleGoToSettings}
onClose={closeModal}
onCancel={closeModal}
icon={null}
variant='warning'
/>,
)
}
/>
);
};

export default StatusDisabledSection;
Loading

1 comment on commit 0d9b086

@TBG-FR
Copy link
Contributor

@TBG-FR TBG-FR commented on 0d9b086 Apr 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A great way to make sure RocketChat users will go away and use another chat software. Limiting free instances to 200 users that way, blocking a major feature of the software, is not really a great choice. 😠

Please sign in to comment.