Skip to content

Commit

Permalink
Merge branch 'develop' into chore/agents-endpoint-public
Browse files Browse the repository at this point in the history
  • Loading branch information
casalsgh authored Oct 23, 2023
2 parents 70983ef + b98b99e commit 13d9270
Show file tree
Hide file tree
Showing 16 changed files with 241 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/hip-pans-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

fix: Omnichannel webhook is not retrying requests
20 changes: 13 additions & 7 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,12 +796,15 @@ class LivechatClass {
attempts = 10,
) {
if (!attempts) {
Livechat.logger.error({ msg: 'Omnichannel webhook call failed. Max attempts reached' });
return;
}
const timeout = settings.get<number>('Livechat_http_timeout');
const secretToken = settings.get<string>('Livechat_secret_token');
const webhookUrl = settings.get<string>('Livechat_webhookUrl');
try {
const result = await fetch(settings.get('Livechat_webhookUrl'), {
Livechat.webhookLogger.debug({ msg: 'Sending webhook request', postData });
const result = await fetch(webhookUrl, {
method: 'POST',
headers: {
...(secretToken && { 'X-RocketChat-Livechat-Token': secretToken }),
Expand All @@ -812,17 +815,20 @@ class LivechatClass {

if (result.status === 200) {
metrics.totalLivechatWebhooksSuccess.inc();
} else {
metrics.totalLivechatWebhooksFailures.inc();
return result;
}
return result;

metrics.totalLivechatWebhooksFailures.inc();
throw new Error(await result.text());
} catch (err) {
Livechat.webhookLogger.error({ msg: `Response error on ${11 - attempts} try ->`, err });
const retryAfter = timeout * 4;
Livechat.webhookLogger.error({ msg: `Error response on ${11 - attempts} try ->`, err });
// try 10 times after 20 seconds each
attempts - 1 && Livechat.webhookLogger.warn(`Will try again in ${(timeout / 1000) * 4} seconds ...`);
attempts - 1 &&
Livechat.webhookLogger.warn({ msg: `Webhook call failed. Retrying`, newAttemptAfterSeconds: retryAfter / 1000, webhookUrl });
setTimeout(async () => {
await Livechat.sendRequest(postData, attempts - 1);
}, timeout * 4);
}, retryAfter);
}
}

Expand Down
20 changes: 18 additions & 2 deletions apps/meteor/client/hooks/useLicense.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
import type { OperationResult } from '@rocket.chat/rest-typings';
import { useEndpoint, usePermission } from '@rocket.chat/ui-contexts';
import { useEndpoint, usePermission, useSingleStream } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';

export const useLicense = (): UseQueryResult<OperationResult<'GET', '/v1/licenses.info'>> => {
const getLicenses = useEndpoint('GET', '/v1/licenses.info');
const canViewLicense = usePermission('view-privileged-setting');

const queryClient = useQueryClient();

const invalidate = useDebouncedCallback(
() => {
queryClient.invalidateQueries(['licenses', 'getLicenses']);
},
5000,
[],
);

const notify = useSingleStream('notify-all');

useEffect(() => notify('license', () => invalidate()), [notify, invalidate]);

return useQuery(
['licenses', 'getLicenses'],
() => {
Expand Down
4 changes: 4 additions & 0 deletions apps/meteor/ee/app/license/server/license.internalService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export class LicenseService extends ServiceClassInternal implements ILicense {
License.onModule((licenseModule) => {
void api.broadcast('license.module', licenseModule);
});

this.onEvent('license.actions', (preventedActions) => License.syncShouldPreventActionResults(preventedActions));

this.onEvent('license.sync', () => License.sync());
}

async started(): Promise<void> {
Expand Down
22 changes: 22 additions & 0 deletions apps/meteor/ee/app/license/server/startup.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { api } from '@rocket.chat/core-services';
import type { LicenseLimitKind } from '@rocket.chat/license';
import { License } from '@rocket.chat/license';
import { Subscriptions, Users, Settings } from '@rocket.chat/models';
import { wrapExceptions } from '@rocket.chat/tools';
Expand Down Expand Up @@ -93,6 +95,26 @@ settings.onReady(async () => {
License.onBehaviorTriggered('start_fair_policy', async (context) => syncByTrigger(`start_fair_policy_${context.limit}`));

License.onBehaviorTriggered('disable_modules', async (context) => syncByTrigger(`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<Partial<LicenseLimitKind>, boolean>);
});

License.onBehaviorToggled('allow_action', (context) => {
if (!context.limit) {
return;
}
void api.broadcast('license.actions', {
[context.limit]: false,
} as Record<Partial<LicenseLimitKind>, boolean>);
});
});

License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount());
Expand Down
19 changes: 9 additions & 10 deletions apps/meteor/packages/rocketchat-mongo-config/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,19 @@ if (Object.keys(mongoConnectionOptions).length > 0) {

process.env.HTTP_FORWARDED_COUNT = process.env.HTTP_FORWARDED_COUNT || '1';

// Send emails to a "fake" stream instead of print them in console in case MAIL_URL or SMTP is not configured
if (process.env.NODE_ENV !== 'development') {
const { sendAsync } = Email;
// Just print to logs if in TEST_MODE due to a bug in Meteor 2.5: TypeError: Cannot read property '_syncSendMail' of null
if (process.env.TEST_MODE === 'true') {
Email.sendAsync = function _sendAsync(options) {
console.log('Email.sendAsync', options);
};
} else if (process.env.NODE_ENV !== 'development') {
// Send emails to a "fake" stream instead of print them in console in case MAIL_URL or SMTP is not configured
const stream = new PassThrough();
stream.on('data', () => {});
stream.on('end', () => {});
Email.sendAsync = function _sendAsync(options) {
return sendAsync.call(this, { stream, ...options });
};
}

// Just print to logs if in TEST_MODE due to a bug in Meteor 2.5: TypeError: Cannot read property '_syncSendMail' of null
if (process.env.TEST_MODE === 'true') {
const { sendAsync } = Email;
Email.sendAsync = function _sendAsync(options) {
console.log('Email.sendAsync', options);
return sendAsync.call(this, { stream, ...options });
};
}
6 changes: 2 additions & 4 deletions apps/meteor/server/lib/dataExport/uploadZipFile.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createReadStream } from 'fs';
import { open, stat } from 'fs/promises';
import { stat } from 'fs/promises';

import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
Expand Down Expand Up @@ -28,9 +28,7 @@ export const uploadZipFile = async (filePath: string, userId: IUser['_id'], expo
name: newFileName,
};

const { fd } = await open(filePath);

const stream = createReadStream('', { fd }); // @todo once upgrades to Node.js v16.x, use createReadStream from fs.promises.open
const stream = createReadStream(filePath);

const userDataStore = FileUpload.getStore('UserDataFiles');

Expand Down
3 changes: 3 additions & 0 deletions apps/meteor/server/modules/listeners/listeners.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export class ListenersModule {
constructor(service: IServiceClass, notifications: NotificationsModule) {
const logger = new Logger('ListenersModule');

service.onEvent('license.sync', () => notifications.notifyAllInThisInstance('license'));
service.onEvent('license.actions', () => notifications.notifyAllInThisInstance('license'));

service.onEvent('emoji.deleteCustom', (emoji) => {
notifications.notifyLoggedInThisInstance('deleteEmojiCustom', {
emojiData: emoji,
Expand Down
2 changes: 2 additions & 0 deletions ee/packages/ddp-client/src/types/streams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import type {
IBanner,
UiKit,
} from '@rocket.chat/core-typings';
import type { LicenseLimitKind } from '@rocket.chat/license';

type ClientAction = 'inserted' | 'updated' | 'removed' | 'changed';

Expand Down Expand Up @@ -69,6 +70,7 @@ export interface StreamerEvents {
{ key: 'public-settings-changed'; args: ['inserted' | 'updated' | 'removed' | 'changed', ISetting] },
{ key: 'deleteCustomSound'; args: [{ soundData: ICustomSound }] },
{ key: 'updateCustomSound'; args: [{ soundData: ICustomSound }] },
{ key: 'license'; args: [{ preventedActions: Record<LicenseLimitKind, boolean> }] | [] },
];

'notify-user': [
Expand Down
64 changes: 64 additions & 0 deletions ee/packages/license/__tests__/emitter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,70 @@ describe('Event License behaviors', () => {
});
});

/**
* This event is used to sync multiple instances of license manager
* The sync event is triggered when the license is changed, but if the validation is running due to a previous change, no sync should be triggered, avoiding multiple/loops syncs
*/
describe('sync event', () => {
it('should emit `sync` event when the license is changed', async () => {
const licenseManager = await getReadyLicenseManager();
const fn = jest.fn();

licenseManager.onChange(fn);

const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
{
max: 10,
behavior: 'prevent_action',
},
{
max: 20,
behavior: 'invalidate_license',
},
]);

await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);

licenseManager.setLicenseLimitCounter('activeUsers', () => 21);

await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);

await expect(fn).toBeCalledTimes(1);
});

it('should not emit `sync` event when the license validation was triggered by a the sync method', async () => {
const licenseManager = await getReadyLicenseManager();
const fn = jest.fn();

licenseManager.onChange(fn);

const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
{
max: 10,
behavior: 'prevent_action',
},
{
max: 20,
behavior: 'invalidate_license',
},
]);

await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);

licenseManager.setLicenseLimitCounter('activeUsers', () => 21);

await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);

await expect(fn).toBeCalledTimes(1);

fn.mockClear();

await expect(licenseManager.sync()).resolves.toBe(undefined);

await expect(fn).toBeCalledTimes(0);
});
});

/**
* this is only called when the prevent_action behavior is triggered for the first time
* it will not be called again until the behavior is toggled
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export type LicenseValidationOptions = {
suppressLog?: boolean;
isNewLicense?: boolean;
context?: Partial<{ [K in LicenseLimitKind]: Partial<LimitContext<LicenseLimitKind>> }>;
triggerSync?: boolean;
};
1 change: 1 addition & 0 deletions ee/packages/license/src/definition/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export type LicenseEvents = ModuleValidation &
validate: undefined;
invalidate: undefined;
module: { module: LicenseModule; valid: boolean };
sync: undefined;
};
7 changes: 7 additions & 0 deletions ee/packages/license/src/events/listeners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import type { LicenseModule } from '../definition/LicenseModule';
import type { LicenseManager } from '../license';
import { hasModule } from '../modules';

/**
* Invoked when the license changes some internal state. it's called to sync the license with other instances.
*/
export function onChange(this: LicenseManager, cb: () => void) {
this.on('sync', cb);
}

export function onValidFeature(this: LicenseManager, feature: LicenseModule, cb: () => void) {
this.on(`valid:${feature}`, cb);

Expand Down
3 changes: 3 additions & 0 deletions ee/packages/license/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
onInvalidateLicense,
onLimitReached,
onModule,
onChange,
onToggledFeature,
onValidFeature,
onValidateLicense,
Expand Down Expand Up @@ -82,6 +83,8 @@ export class LicenseImp extends LicenseManager implements License {
return this.shouldPreventAction(action, 0, context);
}

onChange = onChange;

onValidFeature = onValidFeature;

onInvalidFeature = onInvalidFeature;
Expand Down
Loading

0 comments on commit 13d9270

Please sign in to comment.