Skip to content

Commit

Permalink
Merge branch 'develop' into feat/setup-wizard-register-offline
Browse files Browse the repository at this point in the history
  • Loading branch information
tiagoevanp authored Oct 3, 2023
2 parents 1f5c37e + 3979680 commit 5363590
Show file tree
Hide file tree
Showing 34 changed files with 561 additions and 244 deletions.
13 changes: 13 additions & 0 deletions .changeset/nice-chairs-add.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/core-typings": minor
---

Added `push` statistic, containing three bits. Each bit represents a boolean:
```
1 1 1
| | |
| | +- push enabled = 0b1 = 1
| +--- push gateway enabled = 0b10 = 2
+----- push gateway changed = 0b100 = 4
```
5 changes: 5 additions & 0 deletions .changeset/quiet-phones-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Search users using full name too on share message modal
44 changes: 24 additions & 20 deletions apps/meteor/app/cloud/server/functions/buildRegistrationData.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,52 @@
import type { SettingValue } from '@rocket.chat/core-typings';
import { Statistics, Users } from '@rocket.chat/models';

import { settings } from '../../../settings/server';
import { statistics } from '../../../statistics/server';
import { Info } from '../../../utils/rocketchat.info';
import { LICENSE_VERSION } from '../license';

export type WorkspaceRegistrationData<T> = {
uniqueId: string;
workspaceId: SettingValue;
address: SettingValue;
workspaceId: string;
address: string;
contactName: string;
contactEmail: T;
seats: number;
allowMarketing: SettingValue;
accountName: SettingValue;

organizationType: string;
industry: string;
orgSize: string;
country: string;
language: string;
agreePrivacyTerms: SettingValue;
website: SettingValue;
siteName: SettingValue;
allowMarketing: string;
accountName: string;
agreePrivacyTerms: string;
website: string;
siteName: string;
workspaceType: unknown;
deploymentMethod: string;
deploymentPlatform: string;
version: unknown;
version: string;
licenseVersion: number;
enterpriseReady: boolean;
setupComplete: boolean;
connectionDisable: boolean;
npsEnabled: SettingValue;
npsEnabled: string;
MAC: number;
};

export async function buildWorkspaceRegistrationData<T extends string | undefined>(contactEmail: T): Promise<WorkspaceRegistrationData<T>> {
const stats = (await Statistics.findLast()) || (await statistics.get());

const address = settings.get('Site_Url');
const siteName = settings.get('Site_Name');
const workspaceId = settings.get('Cloud_Workspace_Id');
const allowMarketing = settings.get('Allow_Marketing_Emails');
const accountName = settings.get('Organization_Name');
const website = settings.get('Website');
const npsEnabled = settings.get('NPS_survey_enabled');
const agreePrivacyTerms = settings.get('Cloud_Service_Agree_PrivacyTerms');
const setupWizardState = settings.get('Show_Setup_Wizard');
const address = settings.get<string>('Site_Url');
const siteName = settings.get<string>('Site_Name');
const workspaceId = settings.get<string>('Cloud_Workspace_Id');
const allowMarketing = settings.get<string>('Allow_Marketing_Emails');
const accountName = settings.get<string>('Organization_Name');
const website = settings.get<string>('Website');
const npsEnabled = settings.get<string>('NPS_survey_enabled');
const agreePrivacyTerms = settings.get<string>('Cloud_Service_Agree_PrivacyTerms');
const setupWizardState = settings.get<string>('Show_Setup_Wizard');

const firstUser = await Users.getOldest({ projection: { name: 1, emails: 1 } });
const contactName = firstUser?.name || '';
Expand Down Expand Up @@ -72,11 +74,13 @@ export async function buildWorkspaceRegistrationData<T extends string | undefine
workspaceType: String(workspaceType),
deploymentMethod: stats.deploy.method,
deploymentPlatform: stats.deploy.platform,
version: stats.version,
version: stats.version ?? Info.version,
licenseVersion: LICENSE_VERSION,
enterpriseReady: true,
setupComplete: setupWizardState === 'completed',
connectionDisable: !registerServer,
npsEnabled,
// TODO: add MAC count
MAC: 0,
};
}
8 changes: 4 additions & 4 deletions apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ const fetchCloudWorkspaceLicensePayload = async ({ token }: { token: string }):

export async function getWorkspaceLicense(): Promise<{ updated: boolean; license: string }> {
const currentLicense = await Settings.findOne('Cloud_Workspace_License');
// it should never happen, since even if the license is not found, it will return an empty settings
if (!currentLicense?._updatedAt) {
throw new CloudWorkspaceLicenseError('Failed to retrieve current license');
}

const fromCurrentLicense = async () => {
const license = currentLicense?.value as string | undefined;
Expand All @@ -67,10 +71,6 @@ export async function getWorkspaceLicense(): Promise<{ updated: boolean; license
return fromCurrentLicense();
}

if (!currentLicense?._updatedAt) {
throw new CloudWorkspaceLicenseError('Failed to retrieve current license');
}

const payload = await fetchCloudWorkspaceLicensePayload({ token });

if (Date.parse(payload.updatedAt) <= currentLicense._updatedAt.getTime()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SettingValue } from '@rocket.chat/core-typings';
import { License } from '@rocket.chat/license';
import { Settings } from '@rocket.chat/models';
import type { SupportedVersions } from '@rocket.chat/server-cloud-communication';
import type { SignedSupportedVersions, SupportedVersions } from '@rocket.chat/server-cloud-communication';
import type { Response } from '@rocket.chat/server-fetch';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';

Expand All @@ -10,6 +10,12 @@ import { settings } from '../../../../settings/server';
import { generateWorkspaceBearerHttpHeader } from '../getWorkspaceAccessToken';
import { supportedVersionsChooseLatest } from './supportedVersionsChooseLatest';

declare module '@rocket.chat/license' {
interface ILicenseV3 {
supportedVersions?: SignedSupportedVersions;
}
}

/** HELPERS */

export const wrapPromise = <T>(
Expand Down Expand Up @@ -115,9 +121,10 @@ const getSupportedVersionsToken = async () => {
* return the token
*/

const [versionsFromLicense, response] = await Promise.all([License.supportedVersions(), getSupportedVersionsFromCloud()]);
const [versionsFromLicense, response] = await Promise.all([License.getLicense(), getSupportedVersionsFromCloud()]);

return (await supportedVersionsChooseLatest(versionsFromLicense, (response.success && response.result) || undefined))?.signed;
return (await supportedVersionsChooseLatest(versionsFromLicense?.supportedVersions, (response.success && response.result) || undefined))
?.signed;
};

export const getCachedSupportedVersionsToken = cacheValueInSettings('Cloud_Workspace_Supported_Versions_Token', getSupportedVersionsToken);
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { type Cloud, type Serialized } from '@rocket.chat/core-typings';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { v, compile } from 'suretype';

import { CloudWorkspaceAccessError } from '../../../../../lib/errors/CloudWorkspaceAccessError';
import { CloudWorkspaceConnectionError } from '../../../../../lib/errors/CloudWorkspaceConnectionError';
import { CloudWorkspaceRegistrationError } from '../../../../../lib/errors/CloudWorkspaceRegistrationError';
import { SystemLogger } from '../../../../../server/lib/logger/system';
import { settings } from '../../../../settings/server';
import { buildWorkspaceRegistrationData } from '../buildRegistrationData';
import { getWorkspaceAccessToken } from '../getWorkspaceAccessToken';
import { retrieveRegistrationStatus } from '../retrieveRegistrationStatus';
import { handleAnnouncementsOnWorkspaceSync, handleNpsOnWorkspaceSync } from './handleCommsSync';
import { legacySyncWorkspace } from './legacySyncWorkspace';

const workspaceCommPayloadSchema = v.object({
workspaceId: v.string().required(),
publicKey: v.string(),
nps: v.object({
id: v.string().required(),
startAt: v.string().format('date-time').required(),
expireAt: v.string().format('date-time').required(),
}),
announcements: v.object({
create: v.array(
v.object({
_id: v.string().required(),
_updatedAt: v.string().format('date-time').required(),
selector: v.object({
roles: v.array(v.string()),
}),
platform: v.array(v.string().enum('web', 'mobile')).required(),
expireAt: v.string().format('date-time').required(),
startAt: v.string().format('date-time').required(),
createdBy: v.string().enum('cloud', 'system').required(),
createdAt: v.string().format('date-time').required(),
dictionary: v.object({}).additional(v.object({}).additional(v.string())),
view: v.any(),
surface: v.string().enum('banner', 'modal').required(),
}),
),
delete: v.array(v.string()),
}),
});

const assertWorkspaceCommPayload = compile(workspaceCommPayloadSchema);

const fetchCloudAnnouncementsSync = async ({
token,
data,
}: {
token: string;
data: Cloud.WorkspaceSyncRequestPayload;
}): Promise<Serialized<Cloud.WorkspaceCommsResponsePayload>> => {
const cloudUrl = settings.get<string>('Cloud_Url');
const response = await fetch(`${cloudUrl}/api/v3/comms/workspace`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
},
body: data,
});

if (!response.ok) {
try {
const { error } = await response.json();
throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${error}`);
} catch (error) {
throw new CloudWorkspaceConnectionError(`Failed to connect to Rocket.Chat Cloud: ${response.statusText}`);
}
}

const payload = await response.json();

assertWorkspaceCommPayload(payload);
return payload;
};

export async function announcementSync() {
try {
const { workspaceRegistered } = await retrieveRegistrationStatus();
if (!workspaceRegistered) {
throw new CloudWorkspaceRegistrationError('Workspace is not registered');
}

const token = await getWorkspaceAccessToken(true);
if (!token) {
throw new CloudWorkspaceAccessError('Workspace does not have a valid access token');
}

const workspaceRegistrationData = await buildWorkspaceRegistrationData(undefined);

const { nps, announcements } = await fetchCloudAnnouncementsSync({
token,
data: workspaceRegistrationData,
});

if (nps) {
await handleNpsOnWorkspaceSync(nps);
}

if (announcements) {
await handleAnnouncementsOnWorkspaceSync(announcements);
}

return true;
} catch (err) {
SystemLogger.error({
msg: 'Failed to sync with Rocket.Chat Cloud',
url: '/sync',
err,
});
}

await legacySyncWorkspace();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { NPS, Banner } from '@rocket.chat/core-services';
import { type Cloud, type Serialized } from '@rocket.chat/core-typings';
import { CloudAnnouncements } from '@rocket.chat/models';

import { getAndCreateNpsSurvey } from '../../../../../server/services/nps/getAndCreateNpsSurvey';

export const handleNpsOnWorkspaceSync = async (nps: Exclude<Serialized<Cloud.WorkspaceSyncPayload>['nps'], undefined>) => {
const { id: npsId, expireAt } = nps;

const startAt = new Date(nps.startAt);

await NPS.create({
npsId,
startAt,
expireAt: new Date(expireAt),
createdBy: {
_id: 'rocket.cat',
username: 'rocket.cat',
},
});

const now = new Date();

if (startAt.getFullYear() === now.getFullYear() && startAt.getMonth() === now.getMonth() && startAt.getDate() === now.getDate()) {
await getAndCreateNpsSurvey(npsId);
}
};

export const handleBannerOnWorkspaceSync = async (banners: Exclude<Serialized<Cloud.WorkspaceSyncPayload>['banners'], undefined>) => {
for await (const banner of banners) {
const { createdAt, expireAt, startAt, inactivedAt, _updatedAt, ...rest } = banner;

await Banner.create({
...rest,
createdAt: new Date(createdAt),
expireAt: new Date(expireAt),
startAt: new Date(startAt),
...(inactivedAt && { inactivedAt: new Date(inactivedAt) }),
});
}
};

const deserializeAnnouncement = (announcement: Serialized<Cloud.Announcement>): Cloud.Announcement => ({
...announcement,
_updatedAt: new Date(announcement._updatedAt),
expireAt: new Date(announcement.expireAt),
startAt: new Date(announcement.startAt),
createdAt: new Date(announcement.createdAt),
});

export const handleAnnouncementsOnWorkspaceSync = async (
announcements: Exclude<Serialized<Cloud.WorkspaceCommsResponsePayload>['announcements'], undefined>,
) => {
const { create, delete: deleteIds } = announcements;

if (deleteIds) {
await CloudAnnouncements.deleteMany({ _id: { $in: deleteIds } });
}

for await (const announcement of create.map(deserializeAnnouncement)) {
const { _id, ...rest } = announcement;

await CloudAnnouncements.updateOne({ _id }, { $set: rest }, { upsert: true });
}
};
4 changes: 2 additions & 2 deletions apps/meteor/app/cloud/server/functions/syncWorkspace/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { CloudWorkspaceAccessTokenError } from '../getWorkspaceAccessToken';
import { getWorkspaceLicense } from '../getWorkspaceLicense';
import { getCachedSupportedVersionsToken } from '../supportedVersionsToken/supportedVersionsToken';
import { announcementSync } from './announcementSync';
import { syncCloudData } from './syncCloudData';

export async function syncWorkspace() {
try {
await syncCloudData();
await getWorkspaceLicense();
await announcementSync();
} catch (error) {
if (error instanceof CloudWorkspaceAccessTokenError) {
// TODO: Remove License if there is no access token
Expand Down
Loading

0 comments on commit 5363590

Please sign in to comment.