import { api } from '@rocket.chat/core-services'; import type { LicenseLimitKind } from '@rocket.chat/core-typings'; import { applyLicense, applyLicenseOrRemove, applyNewestLicense, License } from '@rocket.chat/license'; import { Subscriptions, Users, Settings, LivechatContacts } from '@rocket.chat/models'; import { wrapExceptions } from '@rocket.chat/tools'; import moment from 'moment'; import { getAppCount } from './lib/getAppCount'; import { callbacks } from '../../../../server/lib/callbacks'; import { syncWorkspace } from '../../../../server/lib/cloud/syncWorkspace'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { notifyOnSettingChangedById } from '../../../../server/lib/notifyListener'; import { settings } from '../../../../server/settings'; const applyStartupLicense = async (): Promise => { // Licenses can't be validated before the workspace URL is known; set it explicitly // so the startup license is never left pending behind the Site_Url watcher. await License.setWorkspaceUrl(settings.get('Site_Url')); await applyNewestLicense(License, settings.get('Enterprise_License') ?? '', process.env.ROCKETCHAT_LICENSE ?? ''); }; export const startLicense = async () => { settings.watch('Site_Url', (value) => { if (value) { void License.setWorkspaceUrl(value); } }); License.onValidateLicense(async () => { (await Settings.updateValueById('Enterprise_License', License.encryptedLicense)).modifiedCount && void notifyOnSettingChangedById('Enterprise_License'); (await Settings.updateValueById('Enterprise_License_Status', 'Valid')).modifiedCount && void notifyOnSettingChangedById('Enterprise_License_Status'); }); License.onInvalidateLicense(async () => { (await Settings.updateValueById('Enterprise_License_Status', 'Invalid')).modifiedCount && void notifyOnSettingChangedById('Enterprise_License_Status'); }); License.onRemoveLicense(async () => { (await Settings.updateValueById('Enterprise_License', '')).modifiedCount && void notifyOnSettingChangedById('Enterprise_License_Status'); (await Settings.updateValueById('Enterprise_License_Status', 'Invalid')).modifiedCount && void notifyOnSettingChangedById('Enterprise_License_Status'); }); /** * This is a debounced function that will sync the workspace data to the cloud. * it caches the context, waits for a second and then syncs the data. */ const syncByTriggerDebounced = (() => { let timeout: NodeJS.Timeout | undefined; const contexts: Set = new Set(); return async (context: string) => { contexts.add(context); if (timeout) { clearTimeout(timeout); } timeout = setTimeout(() => { timeout = undefined; void syncByTrigger([...contexts]); contexts.clear(); }, 1000); }; })(); const syncByTrigger = async (contexts: string[]) => { if (!License.encryptedLicense) { return; } const existingData = wrapExceptions(() => JSON.parse(settings.get('Enterprise_License_Data'))).catch(() => ({})) ?? {}; const date = new Date(); const day = date.getDate(); const month = date.getMonth() + 1; const year = date.getFullYear(); const period = `${year}-${month}-${day}`; const [, , signed] = License.encryptedLicense.split('.'); // Check if this sync has already been done. Based on License, behavior. if ([...contexts.values()].every((context) => existingData.signed === signed && existingData[context] === period)) { return; } const obj = Object.fromEntries(contexts.map((context) => [context, period])); ( await Settings.updateValueById( 'Enterprise_License_Data', JSON.stringify({ ...(existingData.signed === signed && existingData), ...existingData, ...obj, signed, }), ) ).modifiedCount && void notifyOnSettingChangedById('Enterprise_License_Data'); try { await syncWorkspace(); } catch (error) { console.error(error); } }; License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount()); License.setLicenseLimitCounter('guestUsers', () => Users.getActiveLocalGuestCount()); License.setLicenseLimitCounter('roomsPerGuest', async (context) => context?.userId ? Subscriptions.countByUserIdExceptType(context.userId, 'd') : 0, ); License.setLicenseLimitCounter('privateApps', () => getAppCount('private')); License.setLicenseLimitCounter('marketplaceApps', () => getAppCount('marketplace')); License.setLicenseLimitCounter('monthlyActiveContacts', () => LivechatContacts.countContactsOnPeriod(moment.utc().format('YYYY-MM'))); return new Promise((resolve) => { // When settings are loaded, apply the current license if there is one. settings.onReady(async () => { import('./airGappedRestrictions'); await applyStartupLicense(); License.onInstall(() => { if (License.hasOfflineLicense()) { // startup level so it is visible at the default Log_Level, like 'License installed' SystemLogger.startup( 'Offline license detected: outbound connections to Rocket.Chat Cloud services and the Rocket.Chat Push Gateway are disabled', ); } }); // After the current license is already loaded, watch the setting value to react to new licenses being applied. settings.change('Enterprise_License', (license) => applyLicenseOrRemove(license, true)); callbacks.add('workspaceLicenseRemoved', () => License.remove()); callbacks.add('workspaceLicenseChanged', (updatedLicense) => applyLicense(updatedLicense, true)); License.onInstall(async () => void api.broadcast('license.actions', {} as Record, boolean>)); License.onInvalidate(async () => void api.broadcast('license.actions', {} as Record, boolean>)); License.onBehaviorTriggered('prevent_action', (context) => syncByTriggerDebounced(`prevent_action_${context.limit}`)); License.onBehaviorTriggered('start_fair_policy', async (context) => syncByTriggerDebounced(`start_fair_policy_${context.limit}`)); License.onBehaviorTriggered('disable_modules', async (context) => syncByTriggerDebounced(`disable_modules_${context.limit}`)); License.onChange(() => api.broadcast('license.sync')); License.onBehaviorToggled('prevent_action', (context) => { if (!context.limit) { return; } void api.broadcast('license.actions', { [context.limit]: true, } as Record, boolean>); }); License.onBehaviorToggled('allow_action', (context) => { if (!context.limit) { return; } void api.broadcast('license.actions', { [context.limit]: false, } as Record, boolean>); }); resolve(); }); }); };