From 4759d5bf0a5eb2a2182c456aa0e8dacd7e5c8a5d Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Mon, 26 Dec 2022 16:07:41 -0300 Subject: [PATCH 01/19] feat: list all non sent app requests from an workspace --- apps/meteor/app/apps/client/orchestrator.ts | 14 +++++++++- .../app/apps/server/communication/rest.js | 26 ++++++++++++++++++ .../meteor/client/views/admin/apps/AppMenu.js | 13 ++++++++- packages/core-typings/src/AppRequests.ts | 27 +++++++++++++++++++ packages/core-typings/src/MarketplaceRest.ts | 17 ++++++++++++ packages/core-typings/src/index.ts | 2 ++ packages/rest-typings/src/apps/index.ts | 6 ++++- 7 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 packages/core-typings/src/AppRequests.ts create mode 100644 packages/core-typings/src/MarketplaceRest.ts diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index 6e3591de0be0..e51342dbcfc2 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -7,7 +7,7 @@ import type { IPermission } from '@rocket.chat/apps-engine/definition/permission import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage/IAppStorageItem'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import type { AppScreenshot, Serialized } from '@rocket.chat/core-typings'; +import type { AppScreenshot, AppRequest, AppRequestFilter, Pagination, RestResponse, Serialized } from '@rocket.chat/core-typings'; import type { App } from '../../../client/views/admin/apps/types'; import { dispatchToastMessage } from '../../../client/lib/toast'; @@ -234,6 +234,18 @@ class AppClientOrchestrator { throw new Error('Failed to build external url'); } + public async appRequests(appId: string, filter: AppRequestFilter, sort: string, pagination: Pagination): Promise { + try { + const response = await APIClient.get( + `/apps/app-request?appId=${appId}&filter=${filter}&sort=${sort}&limit=${pagination.limit}&offset=${pagination.offset}`, + ); + + return response; + } catch (e: unknown) { + throw new Error('Could not get app requests'); + } + } + public async getCategories(): Promise> { const result = await APIClient.get('/apps', { categories: 'true' }); diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 1dd088b5cd4f..d261ecbe439f 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -885,5 +885,31 @@ export class AppsRestApi { }, }, ); + + this.api.addRoute( + 'app-request', + { authRequired: true }, + { + async get() { + const baseUrl = orchestrator.getMarketplaceUrl(); + const { appId } = this.queryParams; + const headers = getDefaultHeaders(); + + const token = await getWorkspaceAccessToken(); + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + try { + const data = HTTP.get(`${baseUrl}/v1/app-request?appId=${appId}&filter=notification-not-sent`, { headers }); + return API.v1.success({ data }); + } catch (e) { + orchestrator.getRocketChatLogger().error('Error getting all non sent app requests from the Marketplace:', e.message); + + return API.v1.failure(e.message); + } + }, + }, + ); } } diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index b2119307b408..669f40913d0c 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -48,6 +48,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { const buildExternalUrl = useEndpoint('GET', '/apps'); const syncApp = useEndpoint('POST', `/apps/${app.id}/sync`); const uninstallApp = useEndpoint('DELETE', `/apps/${app.id}`); + const appRequestNotifications = useEndpoint('GET', `/apps/app-request?appId=${app.id}`); const [loading, setLoading] = useState(false); @@ -68,7 +69,17 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { (permissionsGranted) => { setModal(null); - marketplaceActions[action]({ ...app, permissionsGranted }).then(() => { + marketplaceActions[action]({ ...app, permissionsGranted }).then(async () => { + // notify user + if (action === 'install') { + const pagination = { + limit: 10, + offset: 0, + }; + + await Apps.appRequests(app.id, 'notification-not-sent', '-createdDate', pagination); + } + setLoading(false); }); }, diff --git a/packages/core-typings/src/AppRequests.ts b/packages/core-typings/src/AppRequests.ts new file mode 100644 index 000000000000..9e0d77fdb29a --- /dev/null +++ b/packages/core-typings/src/AppRequests.ts @@ -0,0 +1,27 @@ +export type AppRequestFilter = 'unseen' | 'seen' | 'notification-sent' | 'notification-not-sent'; + +export type AppRequestEndUser = { + id: string; + username: string; + name: string; + nickname: string; + emails: string[]; +}; + +export type AppRequest = { + id: string; + appId: string; + + requester: AppRequestEndUser; + admins: AppRequestEndUser[]; + + workspaceId: string; + mesage: string; + + seen: boolean; + seenAt: string; + notificationSent: boolean; + notificationSentAt: string; + + createdAt: string; +}; diff --git a/packages/core-typings/src/MarketplaceRest.ts b/packages/core-typings/src/MarketplaceRest.ts new file mode 100644 index 000000000000..f1f36fa7dc2c --- /dev/null +++ b/packages/core-typings/src/MarketplaceRest.ts @@ -0,0 +1,17 @@ +export type PaginationMeta = { + total: number; + limit: number; + offset: number; + sort: string; + filter: string; +}; + +export type Pagination = { + offset: number; + limit: number; +}; + +export type RestResponse = { + data: any; + meta: PaginationMeta; +}; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index cccac1793e9c..b18656932b3f 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -1,6 +1,8 @@ export * from './Apps'; export * from './AppOverview'; export * from './FeaturedApps'; +export * from './AppRequests'; +export * from './MarketplaceRest'; export * from './IRoom'; export * from './UIKit'; export * from './IMessage'; diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index adc5ee57ed22..838451c41c32 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -4,7 +4,7 @@ import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/ext import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; -import type { AppScreenshot, App, FeaturedAppsSection, ILogItem } from '@rocket.chat/core-typings'; +import type { AppScreenshot, App, FeaturedAppsSection, ILogItem, AppRequest, PaginationMeta, Pagination, AppRequestFilter, RestResponse } from '@rocket.chat/core-typings'; export type AppsEndpoints = { '/apps/externalComponents': { @@ -119,6 +119,10 @@ export type AppsEndpoints = { }; }; + '/apps/app-request': { + GET: (params: { appId: string; filter: AppRequestFilter; sort: string; pagination: Pagination }) => RestResponse; + }; + '/apps': { GET: | ((params: { buildExternalUrl: 'true'; purchaseType?: 'buy' | 'subscription'; appId?: string; details?: 'true' | 'false' }) => { From a3c3b00f97d6cc2be4f5f4aa3dbc4890d03c7f6b Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Tue, 27 Dec 2022 00:38:58 -0300 Subject: [PATCH 02/19] feat: add a function to notify users via rocket.cat and call it when an app is installed --- apps/meteor/app/apps/client/orchestrator.ts | 17 ++++- .../app/apps/server/communication/rest.js | 23 ++++++- .../meteor/client/views/admin/apps/AppMenu.js | 9 +-- .../admin/apps/helpers/notifyAppRequests.ts | 64 +++++++++++++++++++ .../meteor/server/lib/sendMessagesToAdmins.ts | 2 +- apps/meteor/server/lib/sendMessagesToUsers.ts | 32 ++++++++++ packages/rest-typings/src/apps/index.ts | 14 +++- 7 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts create mode 100644 apps/meteor/server/lib/sendMessagesToUsers.ts diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index e51342dbcfc2..3b7443e296c5 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -7,7 +7,7 @@ import type { IPermission } from '@rocket.chat/apps-engine/definition/permission import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage/IAppStorageItem'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import type { AppScreenshot, AppRequest, AppRequestFilter, Pagination, RestResponse, Serialized } from '@rocket.chat/core-typings'; +import type { AppScreenshot, AppRequestFilter, Pagination, RestResponse, Serialized } from '@rocket.chat/core-typings'; import type { App } from '../../../client/views/admin/apps/types'; import { dispatchToastMessage } from '../../../client/lib/toast'; @@ -240,12 +240,25 @@ class AppClientOrchestrator { `/apps/app-request?appId=${appId}&filter=${filter}&sort=${sort}&limit=${pagination.limit}&offset=${pagination.offset}`, ); - return response; + const restResponse = { + data: response.data.data.data, + meta: response.data.data.meta, + }; + + return restResponse; } catch (e: unknown) { throw new Error('Could not get app requests'); } } + public async notifyUsers(userIds: string[], app: App): Promise { + try { + await APIClient.post('/apps/app-request/notify-users', { userIds, appName: app.name }); + } catch (e: unknown) { + throw new Error('Could not notify end users'); + } + } + public async getCategories(): Promise> { const result = await APIClient.get('/apps', { categories: 'true' }); diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index d261ecbe439f..27e1299eb631 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -12,6 +12,7 @@ import { Apps } from '../orchestrator'; import { formatAppInstanceForRest } from '../../lib/misc/formatAppInstanceForRest'; import { actionButtonsHandler } from './endpoints/actionButtonsHandler'; import { fetch } from '../../../../server/lib/http/fetch'; +import { sendMessagesToUsers } from '../../../../server/lib/sendMessagesToUsers'; const rocketChatVersion = Info.version; const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, ''); @@ -892,7 +893,7 @@ export class AppsRestApi { { async get() { const baseUrl = orchestrator.getMarketplaceUrl(); - const { appId } = this.queryParams; + const { appId, filter, sort, limit, offset } = this.queryParams; const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); @@ -901,7 +902,10 @@ export class AppsRestApi { } try { - const data = HTTP.get(`${baseUrl}/v1/app-request?appId=${appId}&filter=notification-not-sent`, { headers }); + const data = HTTP.get( + `${baseUrl}/v1/app-request?appId=${appId}&filter=${filter}&sort=${sort}&limit=${limit}&offset=${offset}`, + { headers }, + ); return API.v1.success({ data }); } catch (e) { orchestrator.getRocketChatLogger().error('Error getting all non sent app requests from the Marketplace:', e.message); @@ -911,5 +915,20 @@ export class AppsRestApi { }, }, ); + + this.api.addRoute( + 'app-request/notify-users', + { authRequired: true }, + { + async post() { + if (this.bodyParams.userIds && this.bodyParams.appName) { + const msg = `The app you requested, ${this.bodyParams.appName} has just been installed on this workspace.`; + const msgFn = () => ({ msg }); + + await sendMessagesToUsers('rocket.cat', this.bodyParams.userIds, msgFn); + } + }, + }, + ); } } diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index 669f40913d0c..0afd2f2482db 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -19,6 +19,7 @@ import CloudLoginModal from './CloudLoginModal'; import IframeModal from './IframeModal'; import { appEnabledStatuses, handleAPIError, appButtonProps, handleInstallError, warnEnableDisableApp } from './helpers'; import { marketplaceActions } from './helpers/marketplaceActions'; +import { batchAppRequests } from './helpers/notifyAppRequests'; const openIncompatibleModal = async (app, action, cancel, setModal) => { try { @@ -48,7 +49,6 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { const buildExternalUrl = useEndpoint('GET', '/apps'); const syncApp = useEndpoint('POST', `/apps/${app.id}/sync`); const uninstallApp = useEndpoint('DELETE', `/apps/${app.id}`); - const appRequestNotifications = useEndpoint('GET', `/apps/app-request?appId=${app.id}`); const [loading, setLoading] = useState(false); @@ -72,12 +72,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { marketplaceActions[action]({ ...app, permissionsGranted }).then(async () => { // notify user if (action === 'install') { - const pagination = { - limit: 10, - offset: 0, - }; - - await Apps.appRequests(app.id, 'notification-not-sent', '-createdDate', pagination); + batchAppRequests(app); } setLoading(false); diff --git a/apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts b/apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts new file mode 100644 index 000000000000..d240f0bbc78a --- /dev/null +++ b/apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts @@ -0,0 +1,64 @@ +import type { App, AppRequest, RestResponse } from '@rocket.chat/core-typings'; + +import { Apps } from '../../../../../app/apps/client/orchestrator'; + +const notifyBatchOfUsersError = (error: unknown) => { + console.log('error', error); +}; + +const notifyBatchOfUsers = async (app: App, appRequests: RestResponse) => { + const batchRequesters = appRequests.data.reduce((acc: string[], appRequest: AppRequest) => { + // Prevent duplicate requesters + if (!acc.includes(appRequest.requester.id)) { + acc.push(appRequest.requester.id); + } + + return acc; + }, []); + + // Notify users via rocket.cat + Apps.notifyUsers(batchRequesters, app); +}; + +export const batchAppRequests = async (app: App): Promise => { + try { + // First request + const pagination = { + limit: 50, + offset: 0, + }; + + // First request to get the total and the first batch + const appRequests = await Apps.appRequests(app.id, 'notification-not-sent', '-createdDate', pagination); + const { total } = appRequests.meta; + + if (total === 0) { + return; + } + + // Calculate the number of loops - 1 because the first request was already made + const loops = Math.ceil(total / pagination.limit) - 1; + const requestsCollection = []; + + // Notify first batch + requestsCollection.push( + Promise.resolve(appRequests) + .then((response) => notifyBatchOfUsers(app, response)) + .catch(notifyBatchOfUsersError), + ); + + // Batch requests + for (let i = 0; i < loops; i++) { + pagination.offset += pagination.limit; + + const request = Apps.appRequests(app.id, 'notification-not-sent', '-createdDate', pagination); + + request.then((response) => notifyBatchOfUsers(app, response)).catch(notifyBatchOfUsersError); + requestsCollection.push(request); + } + + await Promise.all(requestsCollection); + } catch (e) { + throw e; + } +}; diff --git a/apps/meteor/server/lib/sendMessagesToAdmins.ts b/apps/meteor/server/lib/sendMessagesToAdmins.ts index c74b97d4ea1f..64731eda984b 100644 --- a/apps/meteor/server/lib/sendMessagesToAdmins.ts +++ b/apps/meteor/server/lib/sendMessagesToAdmins.ts @@ -15,7 +15,7 @@ type Banner = { link: string; }; -const getData = (param: T[] | ((params: { adminUser: IUser }) => T[]), adminUser: IUser): T[] => { +export const getData = (param: T[] | ((params: { adminUser: IUser }) => T[]), adminUser: IUser): T[] => { const result = typeof param === 'function' ? param({ adminUser }) : param; if (!Array.isArray(result)) { diff --git a/apps/meteor/server/lib/sendMessagesToUsers.ts b/apps/meteor/server/lib/sendMessagesToUsers.ts new file mode 100644 index 000000000000..f1494186192a --- /dev/null +++ b/apps/meteor/server/lib/sendMessagesToUsers.ts @@ -0,0 +1,32 @@ +import type { IUser, IMessage } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; + +import { SystemLogger } from './logger/system'; +import { executeSendMessage } from '../../app/lib/server/methods/sendMessage'; +import { createDirectMessage } from '../methods/createDirectMessage'; +import { getData } from './sendMessagesToAdmins'; + +export async function sendMessagesToUsers( + fromId = 'rocket.cat', + toIds: string[], + msgs: Partial[] | ((params: { adminUser: IUser }) => Partial[]), +): Promise { + const fromUser = await Users.findOneById(fromId, { projection: { _id: 1 } }); + if (!fromUser) { + throw new Error(`User not found: ${fromId}`); + } + + const users = await Users.findByIds(toIds, { projection: { _id: 1, username: 1 } }).toArray(); + + users.forEach((user) => { + try { + const { rid } = createDirectMessage([user.username], fromId); + + getData>(msgs, user).forEach((msg) => { + executeSendMessage(fromId, Object.assign({ rid }, msg)); + }); + } catch (error) { + SystemLogger.error(error); + } + }); +} diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index 838451c41c32..4f50f1c20441 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -4,7 +4,15 @@ import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/ext import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; -import type { AppScreenshot, App, FeaturedAppsSection, ILogItem, AppRequest, PaginationMeta, Pagination, AppRequestFilter, RestResponse } from '@rocket.chat/core-typings'; +import type { + AppScreenshot, + App, + FeaturedAppsSection, + ILogItem, + Pagination, + AppRequestFilter, + RestResponse, +} from '@rocket.chat/core-typings'; export type AppsEndpoints = { '/apps/externalComponents': { @@ -123,6 +131,10 @@ export type AppsEndpoints = { GET: (params: { appId: string; filter: AppRequestFilter; sort: string; pagination: Pagination }) => RestResponse; }; + '/apps/app-request/notify-users': { + POST: (params: { userIds: string[] }) => void; + }; + '/apps': { GET: | ((params: { buildExternalUrl: 'true'; purchaseType?: 'buy' | 'subscription'; appId?: string; details?: 'true' | 'false' }) => { From 5eaa624b113c657682a092583336070554a969eb Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Tue, 27 Dec 2022 13:00:52 -0300 Subject: [PATCH 03/19] feat: add i18n text to app request end users notification --- apps/meteor/app/apps/client/orchestrator.ts | 2 +- .../app/apps/server/communication/rest.js | 18 ++++++++++++++---- apps/meteor/client/views/admin/apps/AppMenu.js | 4 ++-- .../admin/apps/helpers/notifyAppRequests.ts | 9 +++------ .../packages/rocketchat-i18n/i18n/en.i18n.json | 1 + apps/meteor/server/lib/sendMessagesToUsers.ts | 2 +- packages/rest-typings/src/apps/index.ts | 2 +- 7 files changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index 3b7443e296c5..f27e5bd98f77 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -253,7 +253,7 @@ class AppClientOrchestrator { public async notifyUsers(userIds: string[], app: App): Promise { try { - await APIClient.post('/apps/app-request/notify-users', { userIds, appName: app.name }); + await APIClient.post('/apps/app-request/notify-users', { userIds: userIds, appName: app.name }); } catch (e: unknown) { throw new Error('Could not notify end users'); } diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 27e1299eb631..5912b97ee986 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; import { Settings } from '@rocket.chat/models'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { API } from '../../../api/server'; import { getUploadFormData } from '../../../api/server/lib/getUploadFormData'; @@ -921,12 +922,21 @@ export class AppsRestApi { { authRequired: true }, { async post() { - if (this.bodyParams.userIds && this.bodyParams.appName) { - const msg = `The app you requested, ${this.bodyParams.appName} has just been installed on this workspace.`; - const msgFn = () => ({ msg }); + if (!this.bodyParams.userIds) { + return API.v1.failure('bad request, missing userIds'); + } - await sendMessagesToUsers('rocket.cat', this.bodyParams.userIds, msgFn); + if (!this.bodyParams.appName) { + return API.v1.failure('bad request, missing appName'); } + + const msgFn = (user) => { + const msg = `${TAPi18n.__('App_request_enduser_message', { appname: this.bodyParams.appName }, { lang: user.language })}`; + + return { msg }; + }; + + await sendMessagesToUsers('rocket.cat', this.bodyParams.userIds, msgFn); }, }, ); diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index 0afd2f2482db..be2e09e213c1 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -19,7 +19,7 @@ import CloudLoginModal from './CloudLoginModal'; import IframeModal from './IframeModal'; import { appEnabledStatuses, handleAPIError, appButtonProps, handleInstallError, warnEnableDisableApp } from './helpers'; import { marketplaceActions } from './helpers/marketplaceActions'; -import { batchAppRequests } from './helpers/notifyAppRequests'; +import { appRequestNotificationForUsers } from './helpers/notifyAppRequests'; const openIncompatibleModal = async (app, action, cancel, setModal) => { try { @@ -72,7 +72,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { marketplaceActions[action]({ ...app, permissionsGranted }).then(async () => { // notify user if (action === 'install') { - batchAppRequests(app); + appRequestNotificationForUsers(app); } setLoading(false); diff --git a/apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts b/apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts index d240f0bbc78a..6a032d0ec233 100644 --- a/apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts +++ b/apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts @@ -1,4 +1,4 @@ -import type { App, AppRequest, RestResponse } from '@rocket.chat/core-typings'; +import type { App, AppRequest, Pagination, RestResponse } from '@rocket.chat/core-typings'; import { Apps } from '../../../../../app/apps/client/orchestrator'; @@ -20,13 +20,10 @@ const notifyBatchOfUsers = async (app: App, appRequests: RestResponse) => { Apps.notifyUsers(batchRequesters, app); }; -export const batchAppRequests = async (app: App): Promise => { +export const appRequestNotificationForUsers = async (app: App): Promise => { try { // First request - const pagination = { - limit: 50, - offset: 0, - }; + const pagination: Pagination = { limit: 50, offset: 0 }; // First request to get the total and the first batch const appRequests = await Apps.appRequests(app.id, 'notification-not-sent', '-createdDate', pagination); diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 99e48ccefec0..7a34656fd603 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -5110,6 +5110,7 @@ "Version": "Version", "Version_version": "Version __version__", "App_version_incompatible_tooltip": "App incompatible with Rocket.Chat version", + "App_request_enduser_message": "The app you requested, __appname__ has just been installed on this workspace.", "Video_Conference_Description": "Configure conferencing calls for your workspace.", "Video_Chat_Window": "Video Chat", "Video_Conference": "Conference Call", diff --git a/apps/meteor/server/lib/sendMessagesToUsers.ts b/apps/meteor/server/lib/sendMessagesToUsers.ts index f1494186192a..7a60b4510410 100644 --- a/apps/meteor/server/lib/sendMessagesToUsers.ts +++ b/apps/meteor/server/lib/sendMessagesToUsers.ts @@ -16,7 +16,7 @@ export async function sendMessagesToUsers( throw new Error(`User not found: ${fromId}`); } - const users = await Users.findByIds(toIds, { projection: { _id: 1, username: 1 } }).toArray(); + const users = await Users.findByIds(toIds, { projection: { _id: 1, username: 1, language: 1 } }).toArray(); users.forEach((user) => { try { diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index 4f50f1c20441..08dcdfdc969d 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -132,7 +132,7 @@ export type AppsEndpoints = { }; '/apps/app-request/notify-users': { - POST: (params: { userIds: string[] }) => void; + POST: (params: { userIds: string[]; appName: string }) => void; }; '/apps': { From 8a4ff2d277c24f693c6b696478782ab4f4946fb8 Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Tue, 27 Dec 2022 14:54:55 -0300 Subject: [PATCH 04/19] feat: mark all app requests for end users as sent --- apps/meteor/app/apps/client/orchestrator.ts | 8 +++-- .../app/apps/server/communication/rest.js | 32 +++++++++++++++---- apps/meteor/server/lib/sendMessagesToUsers.ts | 7 +++- packages/rest-typings/src/apps/index.ts | 4 +-- 4 files changed, 40 insertions(+), 11 deletions(-) diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index f27e5bd98f77..26ebd39a28a8 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -237,7 +237,7 @@ class AppClientOrchestrator { public async appRequests(appId: string, filter: AppRequestFilter, sort: string, pagination: Pagination): Promise { try { const response = await APIClient.get( - `/apps/app-request?appId=${appId}&filter=${filter}&sort=${sort}&limit=${pagination.limit}&offset=${pagination.offset}`, + `/apps/app-request?appId=${appId}&q=${filter}&sort=${sort}&limit=${pagination.limit}&offset=${pagination.offset}`, ); const restResponse = { @@ -253,7 +253,11 @@ class AppClientOrchestrator { public async notifyUsers(userIds: string[], app: App): Promise { try { - await APIClient.post('/apps/app-request/notify-users', { userIds: userIds, appName: app.name }); + await APIClient.post('/apps/app-request/notify-users', { + userIds, + appName: app.name, + appId: app.id, + }); } catch (e: unknown) { throw new Error('Could not notify end users'); } diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 5912b97ee986..b8c7370a5f70 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -894,7 +894,7 @@ export class AppsRestApi { { async get() { const baseUrl = orchestrator.getMarketplaceUrl(); - const { appId, filter, sort, limit, offset } = this.queryParams; + const { appId, q, sort, limit, offset } = this.queryParams; const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); @@ -903,10 +903,9 @@ export class AppsRestApi { } try { - const data = HTTP.get( - `${baseUrl}/v1/app-request?appId=${appId}&filter=${filter}&sort=${sort}&limit=${limit}&offset=${offset}`, - { headers }, - ); + const data = HTTP.get(`${baseUrl}/v1/app-request?appId=${appId}&q=${q}&sort=${sort}&limit=${limit}&offset=${offset}`, { + headers, + }); return API.v1.success({ data }); } catch (e) { orchestrator.getRocketChatLogger().error('Error getting all non sent app requests from the Marketplace:', e.message); @@ -936,7 +935,28 @@ export class AppsRestApi { return { msg }; }; - await sendMessagesToUsers('rocket.cat', this.bodyParams.userIds, msgFn); + try { + const success = await sendMessagesToUsers('rocket.cat', this.bodyParams.userIds, msgFn); + const baseUrl = orchestrator.getMarketplaceUrl(); + const headers = getDefaultHeaders(); + const token = await getWorkspaceAccessToken(); + + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + // Mark all success messages for users as sent + await HTTP.post(`${baseUrl}/v1/app-request/markAsSent`, { + data: { + appId: this.bodyParams.appId, + userIds: success, + }, + headers, + }); + } catch (e) { + orchestrator.getRocketChatLogger().error('Error sending app request notification to users:', e.message); + return API.v1.failure(e.message); + } }, }, ); diff --git a/apps/meteor/server/lib/sendMessagesToUsers.ts b/apps/meteor/server/lib/sendMessagesToUsers.ts index 7a60b4510410..508ccd77087e 100644 --- a/apps/meteor/server/lib/sendMessagesToUsers.ts +++ b/apps/meteor/server/lib/sendMessagesToUsers.ts @@ -10,13 +10,14 @@ export async function sendMessagesToUsers( fromId = 'rocket.cat', toIds: string[], msgs: Partial[] | ((params: { adminUser: IUser }) => Partial[]), -): Promise { +): Promise { const fromUser = await Users.findOneById(fromId, { projection: { _id: 1 } }); if (!fromUser) { throw new Error(`User not found: ${fromId}`); } const users = await Users.findByIds(toIds, { projection: { _id: 1, username: 1, language: 1 } }).toArray(); + const success: string[] = []; users.forEach((user) => { try { @@ -24,9 +25,13 @@ export async function sendMessagesToUsers( getData>(msgs, user).forEach((msg) => { executeSendMessage(fromId, Object.assign({ rid }, msg)); + + success.push(user._id); }); } catch (error) { SystemLogger.error(error); } }); + + return success; } diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index 08dcdfdc969d..de70d48e7a29 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -128,11 +128,11 @@ export type AppsEndpoints = { }; '/apps/app-request': { - GET: (params: { appId: string; filter: AppRequestFilter; sort: string; pagination: Pagination }) => RestResponse; + GET: (params: { appId: string; q: AppRequestFilter; sort: string; pagination: Pagination }) => RestResponse; }; '/apps/app-request/notify-users': { - POST: (params: { userIds: string[]; appName: string }) => void; + POST: (params: { userIds: string[]; appName: string }) => string[]; }; '/apps': { From 38a653200a1e499bb9b07cff3d775d1bf6b00205 Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Tue, 27 Dec 2022 19:08:47 -0300 Subject: [PATCH 05/19] feat: handle all messag sending on server --- apps/meteor/app/apps/client/orchestrator.ts | 7 +- .../app/apps/server/communication/rest.js | 38 +----- .../marketplace/appRequestNotifyUsers.ts | 116 ++++++++++++++++++ .../meteor/client/views/admin/apps/AppMenu.js | 3 +- .../admin/apps/helpers/notifyAppRequests.ts | 61 --------- .../meteor/server/lib/sendMessagesToAdmins.ts | 2 +- apps/meteor/server/lib/sendMessagesToUsers.ts | 19 ++- packages/core-typings/src/MarketplaceRest.ts | 6 +- packages/rest-typings/src/apps/index.ts | 5 +- 9 files changed, 140 insertions(+), 117 deletions(-) create mode 100644 apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts delete mode 100644 apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index 26ebd39a28a8..2f4ce4d359e7 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -7,7 +7,7 @@ import type { IPermission } from '@rocket.chat/apps-engine/definition/permission import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage/IAppStorageItem'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import type { AppScreenshot, AppRequestFilter, Pagination, RestResponse, Serialized } from '@rocket.chat/core-typings'; +import type { AppScreenshot, AppRequestFilter, Pagination, IRestResponse, Serialized } from '@rocket.chat/core-typings'; import type { App } from '../../../client/views/admin/apps/types'; import { dispatchToastMessage } from '../../../client/lib/toast'; @@ -234,7 +234,7 @@ class AppClientOrchestrator { throw new Error('Failed to build external url'); } - public async appRequests(appId: string, filter: AppRequestFilter, sort: string, pagination: Pagination): Promise { + public async appRequests(appId: string, filter: AppRequestFilter, sort: string, pagination: Pagination): Promise { try { const response = await APIClient.get( `/apps/app-request?appId=${appId}&q=${filter}&sort=${sort}&limit=${pagination.limit}&offset=${pagination.offset}`, @@ -251,10 +251,9 @@ class AppClientOrchestrator { } } - public async notifyUsers(userIds: string[], app: App): Promise { + public async notifyUsers(app: App): Promise { try { await APIClient.post('/apps/app-request/notify-users', { - userIds, appName: app.name, appId: app.id, }); diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index b8c7370a5f70..24841bc47650 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -1,7 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; import { Settings } from '@rocket.chat/models'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { API } from '../../../api/server'; import { getUploadFormData } from '../../../api/server/lib/getUploadFormData'; @@ -13,7 +12,7 @@ import { Apps } from '../orchestrator'; import { formatAppInstanceForRest } from '../../lib/misc/formatAppInstanceForRest'; import { actionButtonsHandler } from './endpoints/actionButtonsHandler'; import { fetch } from '../../../../server/lib/http/fetch'; -import { sendMessagesToUsers } from '../../../../server/lib/sendMessagesToUsers'; +import { appRequestNotififyForUsers } from '../marketplace/appRequestNotifyUsers'; const rocketChatVersion = Info.version; const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, ''); @@ -906,6 +905,7 @@ export class AppsRestApi { const data = HTTP.get(`${baseUrl}/v1/app-request?appId=${appId}&q=${q}&sort=${sort}&limit=${limit}&offset=${offset}`, { headers, }); + return API.v1.success({ data }); } catch (e) { orchestrator.getRocketChatLogger().error('Error getting all non sent app requests from the Marketplace:', e.message); @@ -921,42 +921,16 @@ export class AppsRestApi { { authRequired: true }, { async post() { - if (!this.bodyParams.userIds) { - return API.v1.failure('bad request, missing userIds'); + if (!this.bodyParams.appId) { + return API.v1.failure('bad request, missing appId'); } if (!this.bodyParams.appName) { return API.v1.failure('bad request, missing appName'); } - const msgFn = (user) => { - const msg = `${TAPi18n.__('App_request_enduser_message', { appname: this.bodyParams.appName }, { lang: user.language })}`; - - return { msg }; - }; - - try { - const success = await sendMessagesToUsers('rocket.cat', this.bodyParams.userIds, msgFn); - const baseUrl = orchestrator.getMarketplaceUrl(); - const headers = getDefaultHeaders(); - const token = await getWorkspaceAccessToken(); - - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - // Mark all success messages for users as sent - await HTTP.post(`${baseUrl}/v1/app-request/markAsSent`, { - data: { - appId: this.bodyParams.appId, - userIds: success, - }, - headers, - }); - } catch (e) { - orchestrator.getRocketChatLogger().error('Error sending app request notification to users:', e.message); - return API.v1.failure(e.message); - } + const baseUrl = orchestrator.getMarketplaceUrl(); + await appRequestNotififyForUsers(baseUrl, this.bodyParams.appId, this.bodyParams.appName); }, }, ); diff --git a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts new file mode 100644 index 000000000000..05651ba1ecbc --- /dev/null +++ b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts @@ -0,0 +1,116 @@ +import { HTTP } from 'meteor/http'; +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import type { AppRequest, IUser, Pagination } from '@rocket.chat/core-typings'; + +import { API } from '../../../api/server'; +import { getWorkspaceAccessToken } from '../../../cloud/server'; +import { Info } from '../../../utils'; +import { sendMessagesToUsers } from '../../../../server/lib/sendMessagesToUsers'; + +const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, ''); +const getDefaultHeaders = () => ({ + 'X-Apps-Engine-Version': appsEngineVersionForMarketplace, + 'Authorization': '', +}); + +const notifyBatchOfUsersError = (error: unknown) => { + throw error; +}; + +const notifyBatchOfUsers = async (marketplaceBaseUrl: string, appId: string, appName: string, appRequests: AppRequest[]) => { + const batchRequesters = appRequests.reduce((acc: string[], appRequest: AppRequest) => { + // Prevent duplicate requesters + if (!acc.includes(appRequest.requester.id)) { + acc.push(appRequest.requester.id); + } + + return acc; + }, []); + + const headers = getDefaultHeaders(); + const token = await getWorkspaceAccessToken(); + + const msgFn = (user: IUser): string => { + const defaultLang = user.language || 'en'; + const msg = `${TAPi18n.__('App_request_enduser_message', { appname: appName, lng: defaultLang })}`; + + return msg; + }; + + try { + const success = await sendMessagesToUsers('rocket.cat', batchRequesters, msgFn); + + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + // Mark all success messages for users as sent + await HTTP.post(`${marketplaceBaseUrl}/v1/app-request/markAsSent`, { + data: { + appId, + userIds: success, + }, + headers, + }); + } catch (e) { + throw e; + } +}; + +export const appRequestNotififyForUsers = async (marketplaceBaseUrl: string, appId: string, appName: string) => { + try { + const headers = getDefaultHeaders(); + const token = await getWorkspaceAccessToken(); + + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + // First request + const pagination: Pagination = { limit: 5, offset: 0 }; + + // First request to get the total and the first batch + const data = HTTP.get( + `${marketplaceBaseUrl}/v1/app-request?appId=${appId}&q=notification-not-sent&limit=${pagination.limit}&offset=${pagination.offset}`, + { + headers, + }, + ); + + const appRequests = API.v1.success({ data }); + const { total } = appRequests.body.data.data.meta; + + if (total === 0) { + return; + } + + // Calculate the number of loops - 1 because the first request was already made + const loops = Math.ceil(total / pagination.limit) - 1; + const requestsCollection = []; + + // Notify first batch + requestsCollection.push( + Promise.resolve(appRequests.body.data.data.data) + .then((response) => notifyBatchOfUsers(marketplaceBaseUrl, appId, appName, response)) + .catch(notifyBatchOfUsersError), + ); + + // Batch requests + for (let i = 0; i < loops; i++) { + pagination.offset += pagination.limit; + + const request = HTTP.get( + `${marketplaceBaseUrl}/v1/app-request?appId=${appId}&q=notification-not-sent&limit=${pagination.limit}&offset=${pagination.offset}`, + { + headers, + }, + ); + + requestsCollection.push(notifyBatchOfUsers(marketplaceBaseUrl, appId, appName, request.data.data)); + } + + await Promise.all(requestsCollection); + } catch (e) { + throw e; + } +}; diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index be2e09e213c1..e9c72ffc50c7 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -19,7 +19,6 @@ import CloudLoginModal from './CloudLoginModal'; import IframeModal from './IframeModal'; import { appEnabledStatuses, handleAPIError, appButtonProps, handleInstallError, warnEnableDisableApp } from './helpers'; import { marketplaceActions } from './helpers/marketplaceActions'; -import { appRequestNotificationForUsers } from './helpers/notifyAppRequests'; const openIncompatibleModal = async (app, action, cancel, setModal) => { try { @@ -72,7 +71,7 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { marketplaceActions[action]({ ...app, permissionsGranted }).then(async () => { // notify user if (action === 'install') { - appRequestNotificationForUsers(app); + await Apps.notifyUsers(app); } setLoading(false); diff --git a/apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts b/apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts deleted file mode 100644 index 6a032d0ec233..000000000000 --- a/apps/meteor/client/views/admin/apps/helpers/notifyAppRequests.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { App, AppRequest, Pagination, RestResponse } from '@rocket.chat/core-typings'; - -import { Apps } from '../../../../../app/apps/client/orchestrator'; - -const notifyBatchOfUsersError = (error: unknown) => { - console.log('error', error); -}; - -const notifyBatchOfUsers = async (app: App, appRequests: RestResponse) => { - const batchRequesters = appRequests.data.reduce((acc: string[], appRequest: AppRequest) => { - // Prevent duplicate requesters - if (!acc.includes(appRequest.requester.id)) { - acc.push(appRequest.requester.id); - } - - return acc; - }, []); - - // Notify users via rocket.cat - Apps.notifyUsers(batchRequesters, app); -}; - -export const appRequestNotificationForUsers = async (app: App): Promise => { - try { - // First request - const pagination: Pagination = { limit: 50, offset: 0 }; - - // First request to get the total and the first batch - const appRequests = await Apps.appRequests(app.id, 'notification-not-sent', '-createdDate', pagination); - const { total } = appRequests.meta; - - if (total === 0) { - return; - } - - // Calculate the number of loops - 1 because the first request was already made - const loops = Math.ceil(total / pagination.limit) - 1; - const requestsCollection = []; - - // Notify first batch - requestsCollection.push( - Promise.resolve(appRequests) - .then((response) => notifyBatchOfUsers(app, response)) - .catch(notifyBatchOfUsersError), - ); - - // Batch requests - for (let i = 0; i < loops; i++) { - pagination.offset += pagination.limit; - - const request = Apps.appRequests(app.id, 'notification-not-sent', '-createdDate', pagination); - - request.then((response) => notifyBatchOfUsers(app, response)).catch(notifyBatchOfUsersError); - requestsCollection.push(request); - } - - await Promise.all(requestsCollection); - } catch (e) { - throw e; - } -}; diff --git a/apps/meteor/server/lib/sendMessagesToAdmins.ts b/apps/meteor/server/lib/sendMessagesToAdmins.ts index 64731eda984b..c74b97d4ea1f 100644 --- a/apps/meteor/server/lib/sendMessagesToAdmins.ts +++ b/apps/meteor/server/lib/sendMessagesToAdmins.ts @@ -15,7 +15,7 @@ type Banner = { link: string; }; -export const getData = (param: T[] | ((params: { adminUser: IUser }) => T[]), adminUser: IUser): T[] => { +const getData = (param: T[] | ((params: { adminUser: IUser }) => T[]), adminUser: IUser): T[] => { const result = typeof param === 'function' ? param({ adminUser }) : param; if (!Array.isArray(result)) { diff --git a/apps/meteor/server/lib/sendMessagesToUsers.ts b/apps/meteor/server/lib/sendMessagesToUsers.ts index 508ccd77087e..3acdebdd17be 100644 --- a/apps/meteor/server/lib/sendMessagesToUsers.ts +++ b/apps/meteor/server/lib/sendMessagesToUsers.ts @@ -1,16 +1,11 @@ -import type { IUser, IMessage } from '@rocket.chat/core-typings'; +import type { IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import { SystemLogger } from './logger/system'; import { executeSendMessage } from '../../app/lib/server/methods/sendMessage'; import { createDirectMessage } from '../methods/createDirectMessage'; -import { getData } from './sendMessagesToAdmins'; -export async function sendMessagesToUsers( - fromId = 'rocket.cat', - toIds: string[], - msgs: Partial[] | ((params: { adminUser: IUser }) => Partial[]), -): Promise { +export async function sendMessagesToUsers(fromId = 'rocket.cat', toIds: string[], messageFn: (user: IUser) => string): Promise { const fromUser = await Users.findOneById(fromId, { projection: { _id: 1 } }); if (!fromUser) { throw new Error(`User not found: ${fromId}`); @@ -19,15 +14,15 @@ export async function sendMessagesToUsers( const users = await Users.findByIds(toIds, { projection: { _id: 1, username: 1, language: 1 } }).toArray(); const success: string[] = []; - users.forEach((user) => { + users.forEach((user: IUser) => { try { const { rid } = createDirectMessage([user.username], fromId); + const msg = messageFn(user); - getData>(msgs, user).forEach((msg) => { - executeSendMessage(fromId, Object.assign({ rid }, msg)); + console.log('final message', msg); - success.push(user._id); - }); + executeSendMessage(fromId, { rid, msg }); + success.push(user._id); } catch (error) { SystemLogger.error(error); } diff --git a/packages/core-typings/src/MarketplaceRest.ts b/packages/core-typings/src/MarketplaceRest.ts index f1f36fa7dc2c..f25698912e82 100644 --- a/packages/core-typings/src/MarketplaceRest.ts +++ b/packages/core-typings/src/MarketplaceRest.ts @@ -11,7 +11,7 @@ export type Pagination = { limit: number; }; -export type RestResponse = { - data: any; +export interface IRestResponse { + data: T[]; meta: PaginationMeta; -}; +} diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index de70d48e7a29..bea7f8120c4c 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -11,7 +11,8 @@ import type { ILogItem, Pagination, AppRequestFilter, - RestResponse, + IRestResponse, + AppRequest, } from '@rocket.chat/core-typings'; export type AppsEndpoints = { @@ -128,7 +129,7 @@ export type AppsEndpoints = { }; '/apps/app-request': { - GET: (params: { appId: string; q: AppRequestFilter; sort: string; pagination: Pagination }) => RestResponse; + GET: (params: { appId: string; q: AppRequestFilter; sort: string; pagination: Pagination }) => IRestResponse; }; '/apps/app-request/notify-users': { From 2792267e2aec03b4e5aa69ec2c016539fb5c3143 Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Wed, 28 Dec 2022 15:25:17 -0300 Subject: [PATCH 06/19] chore: move mark as sent action, and also making the code simpler --- apps/meteor/app/apps/client/orchestrator.ts | 8 +-- .../app/apps/server/communication/rest.js | 20 +++++- .../marketplace/appRequestNotifyUsers.ts | 63 ++++++------------- .../meteor/client/views/admin/apps/AppMenu.js | 8 +-- ...ToUsers.ts => sendDirectMessageToUsers.ts} | 10 +-- 5 files changed, 53 insertions(+), 56 deletions(-) rename apps/meteor/server/lib/{sendMessagesToUsers.ts => sendDirectMessageToUsers.ts} (78%) diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index 2f4ce4d359e7..6b9ce2fab5f8 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -247,18 +247,18 @@ class AppClientOrchestrator { return restResponse; } catch (e: unknown) { - throw new Error('Could not get app requests'); + throw new Error('Could not get the list of app requests'); } } - public async notifyUsers(app: App): Promise { + public async appRequestsNotifyEndUsers(app: App): Promise { try { - await APIClient.post('/apps/app-request/notify-users', { + return await APIClient.post('/apps/app-request/notify-users', { appName: app.name, appId: app.id, }); } catch (e: unknown) { - throw new Error('Could not notify end users'); + throw new Error('Could not notify end users due to an error'); } } diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 24841bc47650..48290ffaa338 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -929,8 +929,26 @@ export class AppsRestApi { return API.v1.failure('bad request, missing appName'); } + const { appId, appName } = this.bodyParams; const baseUrl = orchestrator.getMarketplaceUrl(); - await appRequestNotififyForUsers(baseUrl, this.bodyParams.appId, this.bodyParams.appName); + const token = await getWorkspaceAccessToken(); + const headers = { + Authorization: `Bearer ${token}`, + }; + + try { + // Notify users + await appRequestNotififyForUsers(baseUrl, appId, appName); + + // Mark all as sent + await HTTP.post(`${baseUrl}/v1/app-request/markAsSent/${appId}`, { headers }); + + return API.v1.success(); + } catch (e) { + orchestrator.getRocketChatLogger().error('Could not notify users who requested the app installation:', e.message); + + return API.v1.failure({ error: e.message }); + } }, }, ); diff --git a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts index 05651ba1ecbc..82cc36ae1e0a 100644 --- a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts +++ b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts @@ -4,20 +4,16 @@ import type { AppRequest, IUser, Pagination } from '@rocket.chat/core-typings'; import { API } from '../../../api/server'; import { getWorkspaceAccessToken } from '../../../cloud/server'; -import { Info } from '../../../utils'; -import { sendMessagesToUsers } from '../../../../server/lib/sendMessagesToUsers'; +import { sendDirectMessageToUsers } from '../../../../server/lib/sendDirectMessageToUsers'; -const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, ''); -const getDefaultHeaders = () => ({ - 'X-Apps-Engine-Version': appsEngineVersionForMarketplace, - 'Authorization': '', -}); +const ROCKET_CAT_USERID = 'rocket.cat'; +const DEFAULT_LIMIT = 100; const notifyBatchOfUsersError = (error: unknown) => { throw error; }; -const notifyBatchOfUsers = async (marketplaceBaseUrl: string, appId: string, appName: string, appRequests: AppRequest[]) => { +const notifyBatchOfUsers = async (appName: string, appRequests: AppRequest[]): Promise => { const batchRequesters = appRequests.reduce((acc: string[], appRequest: AppRequest) => { // Prevent duplicate requesters if (!acc.includes(appRequest.requester.id)) { @@ -27,9 +23,6 @@ const notifyBatchOfUsers = async (marketplaceBaseUrl: string, appId: string, app return acc; }, []); - const headers = getDefaultHeaders(); - const token = await getWorkspaceAccessToken(); - const msgFn = (user: IUser): string => { const defaultLang = user.language || 'en'; const msg = `${TAPi18n.__('App_request_enduser_message', { appname: appName, lng: defaultLang })}`; @@ -38,50 +31,33 @@ const notifyBatchOfUsers = async (marketplaceBaseUrl: string, appId: string, app }; try { - const success = await sendMessagesToUsers('rocket.cat', batchRequesters, msgFn); - - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - // Mark all success messages for users as sent - await HTTP.post(`${marketplaceBaseUrl}/v1/app-request/markAsSent`, { - data: { - appId, - userIds: success, - }, - headers, - }); + return await sendDirectMessageToUsers(ROCKET_CAT_USERID, batchRequesters, msgFn); } catch (e) { throw e; } }; -export const appRequestNotififyForUsers = async (marketplaceBaseUrl: string, appId: string, appName: string) => { +export const appRequestNotififyForUsers = async (marketplaceBaseUrl: string, appId: string, appName: string): Promise => { try { - const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); - - if (token) { - headers.Authorization = `Bearer ${token}`; - } + const headers = { + Authorization: `Bearer ${token}`, + }; // First request - const pagination: Pagination = { limit: 5, offset: 0 }; + const pagination: Pagination = { limit: DEFAULT_LIMIT, offset: 0 }; // First request to get the total and the first batch const data = HTTP.get( `${marketplaceBaseUrl}/v1/app-request?appId=${appId}&q=notification-not-sent&limit=${pagination.limit}&offset=${pagination.offset}`, - { - headers, - }, + { headers }, ); const appRequests = API.v1.success({ data }); const { total } = appRequests.body.data.data.meta; - if (total === 0) { - return; + if (total === undefined || total === 0) { + return []; } // Calculate the number of loops - 1 because the first request was already made @@ -91,7 +67,7 @@ export const appRequestNotififyForUsers = async (marketplaceBaseUrl: string, app // Notify first batch requestsCollection.push( Promise.resolve(appRequests.body.data.data.data) - .then((response) => notifyBatchOfUsers(marketplaceBaseUrl, appId, appName, response)) + .then((response) => notifyBatchOfUsers(appName, response)) .catch(notifyBatchOfUsersError), ); @@ -101,15 +77,16 @@ export const appRequestNotififyForUsers = async (marketplaceBaseUrl: string, app const request = HTTP.get( `${marketplaceBaseUrl}/v1/app-request?appId=${appId}&q=notification-not-sent&limit=${pagination.limit}&offset=${pagination.offset}`, - { - headers, - }, + { headers }, ); - requestsCollection.push(notifyBatchOfUsers(marketplaceBaseUrl, appId, appName, request.data.data)); + requestsCollection.push(notifyBatchOfUsers(appName, request.data.data)); } - await Promise.all(requestsCollection); + const finalResult = await Promise.all(requestsCollection); + + // Return the list of users that were notified + return finalResult.flat(); } catch (e) { throw e; } diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index e9c72ffc50c7..2a97fe7f5b8a 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -69,12 +69,12 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { setModal(null); marketplaceActions[action]({ ...app, permissionsGranted }).then(async () => { - // notify user + setLoading(false); + + // Notify all users which requested the app if (action === 'install') { - await Apps.notifyUsers(app); + await Apps.appRequestsNotifyEndUsers(app); } - - setLoading(false); }); }, [setModal, action, app, setLoading], diff --git a/apps/meteor/server/lib/sendMessagesToUsers.ts b/apps/meteor/server/lib/sendDirectMessageToUsers.ts similarity index 78% rename from apps/meteor/server/lib/sendMessagesToUsers.ts rename to apps/meteor/server/lib/sendDirectMessageToUsers.ts index 3acdebdd17be..c6a37eaf4451 100644 --- a/apps/meteor/server/lib/sendMessagesToUsers.ts +++ b/apps/meteor/server/lib/sendDirectMessageToUsers.ts @@ -5,7 +5,11 @@ import { SystemLogger } from './logger/system'; import { executeSendMessage } from '../../app/lib/server/methods/sendMessage'; import { createDirectMessage } from '../methods/createDirectMessage'; -export async function sendMessagesToUsers(fromId = 'rocket.cat', toIds: string[], messageFn: (user: IUser) => string): Promise { +export async function sendDirectMessageToUsers( + fromId = 'rocket.cat', + toIds: string[], + messageFn: (user: IUser) => string, +): Promise { const fromUser = await Users.findOneById(fromId, { projection: { _id: 1 } }); if (!fromUser) { throw new Error(`User not found: ${fromId}`); @@ -17,9 +21,7 @@ export async function sendMessagesToUsers(fromId = 'rocket.cat', toIds: string[] users.forEach((user: IUser) => { try { const { rid } = createDirectMessage([user.username], fromId); - const msg = messageFn(user); - - console.log('final message', msg); + const msg = typeof messageFn === 'function' ? messageFn(user) : messageFn; executeSendMessage(fromId, { rid, msg }); success.push(user._id); From 77864aebded62d5fa5b49ff310d5b46fa6e1c4fd Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Wed, 28 Dec 2022 15:25:17 -0300 Subject: [PATCH 07/19] chore: move mark as sent action, and also making the code simpler --- apps/meteor/app/apps/client/orchestrator.ts | 8 +-- .../app/apps/server/communication/rest.js | 20 +++++- .../marketplace/appRequestNotifyUsers.ts | 63 ++++++------------- .../meteor/client/views/admin/apps/AppMenu.js | 8 +-- ...ToUsers.ts => sendDirectMessageToUsers.ts} | 10 +-- packages/rest-typings/src/apps/index.ts | 2 +- 6 files changed, 54 insertions(+), 57 deletions(-) rename apps/meteor/server/lib/{sendMessagesToUsers.ts => sendDirectMessageToUsers.ts} (78%) diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index 2f4ce4d359e7..6b9ce2fab5f8 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -247,18 +247,18 @@ class AppClientOrchestrator { return restResponse; } catch (e: unknown) { - throw new Error('Could not get app requests'); + throw new Error('Could not get the list of app requests'); } } - public async notifyUsers(app: App): Promise { + public async appRequestsNotifyEndUsers(app: App): Promise { try { - await APIClient.post('/apps/app-request/notify-users', { + return await APIClient.post('/apps/app-request/notify-users', { appName: app.name, appId: app.id, }); } catch (e: unknown) { - throw new Error('Could not notify end users'); + throw new Error('Could not notify end users due to an error'); } } diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 24841bc47650..48290ffaa338 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -929,8 +929,26 @@ export class AppsRestApi { return API.v1.failure('bad request, missing appName'); } + const { appId, appName } = this.bodyParams; const baseUrl = orchestrator.getMarketplaceUrl(); - await appRequestNotififyForUsers(baseUrl, this.bodyParams.appId, this.bodyParams.appName); + const token = await getWorkspaceAccessToken(); + const headers = { + Authorization: `Bearer ${token}`, + }; + + try { + // Notify users + await appRequestNotififyForUsers(baseUrl, appId, appName); + + // Mark all as sent + await HTTP.post(`${baseUrl}/v1/app-request/markAsSent/${appId}`, { headers }); + + return API.v1.success(); + } catch (e) { + orchestrator.getRocketChatLogger().error('Could not notify users who requested the app installation:', e.message); + + return API.v1.failure({ error: e.message }); + } }, }, ); diff --git a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts index 05651ba1ecbc..82cc36ae1e0a 100644 --- a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts +++ b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts @@ -4,20 +4,16 @@ import type { AppRequest, IUser, Pagination } from '@rocket.chat/core-typings'; import { API } from '../../../api/server'; import { getWorkspaceAccessToken } from '../../../cloud/server'; -import { Info } from '../../../utils'; -import { sendMessagesToUsers } from '../../../../server/lib/sendMessagesToUsers'; +import { sendDirectMessageToUsers } from '../../../../server/lib/sendDirectMessageToUsers'; -const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, ''); -const getDefaultHeaders = () => ({ - 'X-Apps-Engine-Version': appsEngineVersionForMarketplace, - 'Authorization': '', -}); +const ROCKET_CAT_USERID = 'rocket.cat'; +const DEFAULT_LIMIT = 100; const notifyBatchOfUsersError = (error: unknown) => { throw error; }; -const notifyBatchOfUsers = async (marketplaceBaseUrl: string, appId: string, appName: string, appRequests: AppRequest[]) => { +const notifyBatchOfUsers = async (appName: string, appRequests: AppRequest[]): Promise => { const batchRequesters = appRequests.reduce((acc: string[], appRequest: AppRequest) => { // Prevent duplicate requesters if (!acc.includes(appRequest.requester.id)) { @@ -27,9 +23,6 @@ const notifyBatchOfUsers = async (marketplaceBaseUrl: string, appId: string, app return acc; }, []); - const headers = getDefaultHeaders(); - const token = await getWorkspaceAccessToken(); - const msgFn = (user: IUser): string => { const defaultLang = user.language || 'en'; const msg = `${TAPi18n.__('App_request_enduser_message', { appname: appName, lng: defaultLang })}`; @@ -38,50 +31,33 @@ const notifyBatchOfUsers = async (marketplaceBaseUrl: string, appId: string, app }; try { - const success = await sendMessagesToUsers('rocket.cat', batchRequesters, msgFn); - - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - // Mark all success messages for users as sent - await HTTP.post(`${marketplaceBaseUrl}/v1/app-request/markAsSent`, { - data: { - appId, - userIds: success, - }, - headers, - }); + return await sendDirectMessageToUsers(ROCKET_CAT_USERID, batchRequesters, msgFn); } catch (e) { throw e; } }; -export const appRequestNotififyForUsers = async (marketplaceBaseUrl: string, appId: string, appName: string) => { +export const appRequestNotififyForUsers = async (marketplaceBaseUrl: string, appId: string, appName: string): Promise => { try { - const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); - - if (token) { - headers.Authorization = `Bearer ${token}`; - } + const headers = { + Authorization: `Bearer ${token}`, + }; // First request - const pagination: Pagination = { limit: 5, offset: 0 }; + const pagination: Pagination = { limit: DEFAULT_LIMIT, offset: 0 }; // First request to get the total and the first batch const data = HTTP.get( `${marketplaceBaseUrl}/v1/app-request?appId=${appId}&q=notification-not-sent&limit=${pagination.limit}&offset=${pagination.offset}`, - { - headers, - }, + { headers }, ); const appRequests = API.v1.success({ data }); const { total } = appRequests.body.data.data.meta; - if (total === 0) { - return; + if (total === undefined || total === 0) { + return []; } // Calculate the number of loops - 1 because the first request was already made @@ -91,7 +67,7 @@ export const appRequestNotififyForUsers = async (marketplaceBaseUrl: string, app // Notify first batch requestsCollection.push( Promise.resolve(appRequests.body.data.data.data) - .then((response) => notifyBatchOfUsers(marketplaceBaseUrl, appId, appName, response)) + .then((response) => notifyBatchOfUsers(appName, response)) .catch(notifyBatchOfUsersError), ); @@ -101,15 +77,16 @@ export const appRequestNotififyForUsers = async (marketplaceBaseUrl: string, app const request = HTTP.get( `${marketplaceBaseUrl}/v1/app-request?appId=${appId}&q=notification-not-sent&limit=${pagination.limit}&offset=${pagination.offset}`, - { - headers, - }, + { headers }, ); - requestsCollection.push(notifyBatchOfUsers(marketplaceBaseUrl, appId, appName, request.data.data)); + requestsCollection.push(notifyBatchOfUsers(appName, request.data.data)); } - await Promise.all(requestsCollection); + const finalResult = await Promise.all(requestsCollection); + + // Return the list of users that were notified + return finalResult.flat(); } catch (e) { throw e; } diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index e9c72ffc50c7..2a97fe7f5b8a 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -69,12 +69,12 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { setModal(null); marketplaceActions[action]({ ...app, permissionsGranted }).then(async () => { - // notify user + setLoading(false); + + // Notify all users which requested the app if (action === 'install') { - await Apps.notifyUsers(app); + await Apps.appRequestsNotifyEndUsers(app); } - - setLoading(false); }); }, [setModal, action, app, setLoading], diff --git a/apps/meteor/server/lib/sendMessagesToUsers.ts b/apps/meteor/server/lib/sendDirectMessageToUsers.ts similarity index 78% rename from apps/meteor/server/lib/sendMessagesToUsers.ts rename to apps/meteor/server/lib/sendDirectMessageToUsers.ts index 3acdebdd17be..c6a37eaf4451 100644 --- a/apps/meteor/server/lib/sendMessagesToUsers.ts +++ b/apps/meteor/server/lib/sendDirectMessageToUsers.ts @@ -5,7 +5,11 @@ import { SystemLogger } from './logger/system'; import { executeSendMessage } from '../../app/lib/server/methods/sendMessage'; import { createDirectMessage } from '../methods/createDirectMessage'; -export async function sendMessagesToUsers(fromId = 'rocket.cat', toIds: string[], messageFn: (user: IUser) => string): Promise { +export async function sendDirectMessageToUsers( + fromId = 'rocket.cat', + toIds: string[], + messageFn: (user: IUser) => string, +): Promise { const fromUser = await Users.findOneById(fromId, { projection: { _id: 1 } }); if (!fromUser) { throw new Error(`User not found: ${fromId}`); @@ -17,9 +21,7 @@ export async function sendMessagesToUsers(fromId = 'rocket.cat', toIds: string[] users.forEach((user: IUser) => { try { const { rid } = createDirectMessage([user.username], fromId); - const msg = messageFn(user); - - console.log('final message', msg); + const msg = typeof messageFn === 'function' ? messageFn(user) : messageFn; executeSendMessage(fromId, { rid, msg }); success.push(user._id); diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index bea7f8120c4c..b85178bc5e2c 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -133,7 +133,7 @@ export type AppsEndpoints = { }; '/apps/app-request/notify-users': { - POST: (params: { userIds: string[]; appName: string }) => string[]; + POST: (params: { appId: string; appName: string }) => string[]; }; '/apps': { From 09ffbed33a73fe952ee11efadbff1b0eecbacbd5 Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Thu, 29 Dec 2022 14:24:28 -0300 Subject: [PATCH 08/19] ref: change to notify users via a cronjob instead of via client side --- apps/meteor/app/apps/server/cron.js | 44 +++++++++++++++++++ apps/meteor/app/apps/server/orchestrator.js | 8 ++++ .../meteor/client/views/admin/apps/AppMenu.js | 5 --- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/apps/server/cron.js b/apps/meteor/app/apps/server/cron.js index 86a0e74b937e..79cd65028a37 100644 --- a/apps/meteor/app/apps/server/cron.js +++ b/apps/meteor/app/apps/server/cron.js @@ -9,6 +9,7 @@ import { Apps } from './orchestrator'; import { getWorkspaceAccessToken } from '../../cloud/server'; import { Users } from '../../models/server'; import { sendMessagesToAdmins } from '../../../server/lib/sendMessagesToAdmins'; +import { appRequestNotififyForUsers } from './marketplace/appRequestNotifyUsers'; const notifyAdminsAboutInvalidApps = Meteor.bindEnvironment(function _notifyAdminsAboutInvalidApps(apps) { if (!apps) { @@ -103,6 +104,49 @@ export const appsUpdateMarketplaceInfo = Meteor.bindEnvironment(function _appsUp Promise.await(Apps.updateAppsMarketplaceInfo(data).then(notifyAdminsAboutInvalidApps).then(notifyAdminsAboutRenewedApps)); }); +export const appsNotifyAppRequests = Meteor.bindEnvironment(function _appsNotifyAppRequests() { + try { + const installedApps = Promise.await(Apps.installedApps({ enabled: true })); + if (!installedApps || installedApps.length === 0) { + return; + } + + const token = Promise.await(getWorkspaceAccessToken()); + const baseUrl = Apps.getMarketplaceUrl(); + const options = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + + const pendingSentUrl = `${baseUrl}/v1/app-request/sent/pending`; + const result = HTTP.get(pendingSentUrl, options); + const data = result.data?.data; + const filtered = installedApps.filter((app) => data.indexOf(app.getID()) !== -1); + + filtered.forEach((app) => { + const appId = app.getID(); + const appName = app.getName(); + + appRequestNotififyForUsers(baseUrl, appId, appName) + .then(() => HTTP.post(`${baseUrl}/v1/app-request/markAsSent/${appId}`, options)) + .catch((err) => { + Apps.debugLog(`could not send app request notifications for app ${appId}. Error: ${err}`); + }); + }); + } catch (err) { + Apps.debugLog(err); + } +}); + +SyncedCron.add({ + name: 'Apps-Request:check', + schedule: (parser) => parser.text('at 6:00 am'), + job() { + appsNotifyAppRequests(); + }, +}); + SyncedCron.add({ name: 'Apps-Engine:check', schedule: (parser) => parser.text('at 4:00 am'), diff --git a/apps/meteor/app/apps/server/orchestrator.js b/apps/meteor/app/apps/server/orchestrator.js index de3d950025b8..64bcf0fba0a0 100644 --- a/apps/meteor/app/apps/server/orchestrator.js +++ b/apps/meteor/app/apps/server/orchestrator.js @@ -191,6 +191,14 @@ export class AppServerOrchestrator { return this._manager.updateAppsMarketplaceInfo(apps).then(() => this._manager.get()); } + async installedApps(filter = {}) { + if (!this.isLoaded()) { + return; + } + + return this._manager.get(filter); + } + async triggerEvent(event, ...payload) { if (!this.isLoaded()) { return; diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index 2a97fe7f5b8a..bd23333d1ec6 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -70,11 +70,6 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { marketplaceActions[action]({ ...app, permissionsGranted }).then(async () => { setLoading(false); - - // Notify all users which requested the app - if (action === 'install') { - await Apps.appRequestsNotifyEndUsers(app); - } }); }, [setModal, action, app, setLoading], From 389b4b1077c9f2209991b73c68f5752a535dd103 Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Thu, 29 Dec 2022 16:47:15 -0300 Subject: [PATCH 09/19] ref: remove app request notification from client side --- apps/meteor/app/apps/client/orchestrator.ts | 11 ------ .../app/apps/server/communication/rest.js | 38 ------------------- .../meteor/client/views/admin/apps/AppMenu.js | 5 --- packages/rest-typings/src/apps/index.ts | 4 -- 4 files changed, 58 deletions(-) diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index 6b9ce2fab5f8..636653321c98 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -251,17 +251,6 @@ class AppClientOrchestrator { } } - public async appRequestsNotifyEndUsers(app: App): Promise { - try { - return await APIClient.post('/apps/app-request/notify-users', { - appName: app.name, - appId: app.id, - }); - } catch (e: unknown) { - throw new Error('Could not notify end users due to an error'); - } - } - public async getCategories(): Promise> { const result = await APIClient.get('/apps', { categories: 'true' }); diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 48290ffaa338..54f8bf4e68a9 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -12,7 +12,6 @@ import { Apps } from '../orchestrator'; import { formatAppInstanceForRest } from '../../lib/misc/formatAppInstanceForRest'; import { actionButtonsHandler } from './endpoints/actionButtonsHandler'; import { fetch } from '../../../../server/lib/http/fetch'; -import { appRequestNotififyForUsers } from '../marketplace/appRequestNotifyUsers'; const rocketChatVersion = Info.version; const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, ''); @@ -915,42 +914,5 @@ export class AppsRestApi { }, }, ); - - this.api.addRoute( - 'app-request/notify-users', - { authRequired: true }, - { - async post() { - if (!this.bodyParams.appId) { - return API.v1.failure('bad request, missing appId'); - } - - if (!this.bodyParams.appName) { - return API.v1.failure('bad request, missing appName'); - } - - const { appId, appName } = this.bodyParams; - const baseUrl = orchestrator.getMarketplaceUrl(); - const token = await getWorkspaceAccessToken(); - const headers = { - Authorization: `Bearer ${token}`, - }; - - try { - // Notify users - await appRequestNotififyForUsers(baseUrl, appId, appName); - - // Mark all as sent - await HTTP.post(`${baseUrl}/v1/app-request/markAsSent/${appId}`, { headers }); - - return API.v1.success(); - } catch (e) { - orchestrator.getRocketChatLogger().error('Could not notify users who requested the app installation:', e.message); - - return API.v1.failure({ error: e.message }); - } - }, - }, - ); } } diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index 2a97fe7f5b8a..bd23333d1ec6 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -70,11 +70,6 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { marketplaceActions[action]({ ...app, permissionsGranted }).then(async () => { setLoading(false); - - // Notify all users which requested the app - if (action === 'install') { - await Apps.appRequestsNotifyEndUsers(app); - } }); }, [setModal, action, app, setLoading], diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index b85178bc5e2c..87344b980bbe 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -132,10 +132,6 @@ export type AppsEndpoints = { GET: (params: { appId: string; q: AppRequestFilter; sort: string; pagination: Pagination }) => IRestResponse; }; - '/apps/app-request/notify-users': { - POST: (params: { appId: string; appName: string }) => string[]; - }; - '/apps': { GET: | ((params: { buildExternalUrl: 'true'; purchaseType?: 'buy' | 'subscription'; appId?: string; details?: 'true' | 'false' }) => { From a7d65bb42a6bde8b119449ee98adf6f01407edf3 Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Fri, 30 Dec 2022 10:09:07 -0300 Subject: [PATCH 10/19] chore: change cronjob time --- apps/meteor/app/apps/server/cron.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/apps/server/cron.js b/apps/meteor/app/apps/server/cron.js index 79cd65028a37..1d8bf8c29135 100644 --- a/apps/meteor/app/apps/server/cron.js +++ b/apps/meteor/app/apps/server/cron.js @@ -128,20 +128,23 @@ export const appsNotifyAppRequests = Meteor.bindEnvironment(function _appsNotify const appId = app.getID(); const appName = app.getName(); - appRequestNotififyForUsers(baseUrl, appId, appName) - .then(() => HTTP.post(`${baseUrl}/v1/app-request/markAsSent/${appId}`, options)) - .catch((err) => { - Apps.debugLog(`could not send app request notifications for app ${appId}. Error: ${err}`); - }); + Promise.await( + appRequestNotififyForUsers(baseUrl, appId, appName) + .then(() => HTTP.post(`${baseUrl}/v1/app-request/markAsSent/${appId}`, options)) + .catch((err) => { + Apps.debugLog(`could not send app request notifications for app ${appId}. Error: ${err}`); + }), + ); }); } catch (err) { Apps.debugLog(err); } }); +// Scheduling as every 24 hours to avoid multiple instances hiting the marketplace at the same time SyncedCron.add({ - name: 'Apps-Request:check', - schedule: (parser) => parser.text('at 6:00 am'), + name: 'Apps-Request-End-Users:notify', + schedule: (parser) => parser.text('every 24 hours'), job() { appsNotifyAppRequests(); }, From 5c8fa1b5e2dff8254f4c6dc4d956c18e5e7fb2dd Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Fri, 30 Dec 2022 10:56:18 -0300 Subject: [PATCH 11/19] feat: log errors --- apps/meteor/app/apps/server/cron.js | 7 ++++++- .../app/apps/server/marketplace/appRequestNotifyUsers.ts | 8 ++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/apps/server/cron.js b/apps/meteor/app/apps/server/cron.js index 1d8bf8c29135..48ce8dd00aa1 100644 --- a/apps/meteor/app/apps/server/cron.js +++ b/apps/meteor/app/apps/server/cron.js @@ -128,13 +128,18 @@ export const appsNotifyAppRequests = Meteor.bindEnvironment(function _appsNotify const appId = app.getID(); const appName = app.getName(); - Promise.await( + const usersNotified = Promise.await( appRequestNotififyForUsers(baseUrl, appId, appName) .then(() => HTTP.post(`${baseUrl}/v1/app-request/markAsSent/${appId}`, options)) .catch((err) => { Apps.debugLog(`could not send app request notifications for app ${appId}. Error: ${err}`); }), ); + + const errors = usersNotified.filter((batch) => batch instanceof Error); + if (errors.length > 0) { + Apps.debugLog(`Some batches of users could not be notified for app ${appId}. Errors: ${errors}`); + } }); } catch (err) { Apps.debugLog(err); diff --git a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts index 82cc36ae1e0a..4663e11322d2 100644 --- a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts +++ b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts @@ -10,7 +10,7 @@ const ROCKET_CAT_USERID = 'rocket.cat'; const DEFAULT_LIMIT = 100; const notifyBatchOfUsersError = (error: unknown) => { - throw error; + return new Error(`could not notify the batch of users`, { cause: error }); }; const notifyBatchOfUsers = async (appName: string, appRequests: AppRequest[]): Promise => { @@ -37,7 +37,11 @@ const notifyBatchOfUsers = async (appName: string, appRequests: AppRequest[]): P } }; -export const appRequestNotififyForUsers = async (marketplaceBaseUrl: string, appId: string, appName: string): Promise => { +export const appRequestNotififyForUsers = async ( + marketplaceBaseUrl: string, + appId: string, + appName: string, +): Promise<(string | Error)[]> => { try { const token = await getWorkspaceAccessToken(); const headers = { From 5bf6b41eb5c6b36a6fe898864d272deefc24bd26 Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Fri, 30 Dec 2022 13:00:27 -0300 Subject: [PATCH 12/19] fix: typecheck errors --- apps/meteor/app/apps/client/orchestrator.ts | 15 ++++++++++----- .../server/marketplace/appRequestNotifyUsers.ts | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/apps/client/orchestrator.ts b/apps/meteor/app/apps/client/orchestrator.ts index 636653321c98..86b958d8775d 100644 --- a/apps/meteor/app/apps/client/orchestrator.ts +++ b/apps/meteor/app/apps/client/orchestrator.ts @@ -7,7 +7,7 @@ import type { IPermission } from '@rocket.chat/apps-engine/definition/permission import type { IAppStorageItem } from '@rocket.chat/apps-engine/server/storage/IAppStorageItem'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; -import type { AppScreenshot, AppRequestFilter, Pagination, IRestResponse, Serialized } from '@rocket.chat/core-typings'; +import type { AppScreenshot, AppRequestFilter, Pagination, IRestResponse, Serialized, AppRequest } from '@rocket.chat/core-typings'; import type { App } from '../../../client/views/admin/apps/types'; import { dispatchToastMessage } from '../../../client/lib/toast'; @@ -234,15 +234,20 @@ class AppClientOrchestrator { throw new Error('Failed to build external url'); } - public async appRequests(appId: string, filter: AppRequestFilter, sort: string, pagination: Pagination): Promise { + public async appRequests( + appId: string, + filter: AppRequestFilter, + sort: string, + pagination: Pagination, + ): Promise> { try { - const response = await APIClient.get( + const response: IRestResponse = await APIClient.get( `/apps/app-request?appId=${appId}&q=${filter}&sort=${sort}&limit=${pagination.limit}&offset=${pagination.offset}`, ); const restResponse = { - data: response.data.data.data, - meta: response.data.data.meta, + data: response.data, + meta: response.meta, }; return restResponse; diff --git a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts index 4663e11322d2..2d503bc74fa7 100644 --- a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts +++ b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts @@ -9,8 +9,8 @@ import { sendDirectMessageToUsers } from '../../../../server/lib/sendDirectMessa const ROCKET_CAT_USERID = 'rocket.cat'; const DEFAULT_LIMIT = 100; -const notifyBatchOfUsersError = (error: unknown) => { - return new Error(`could not notify the batch of users`, { cause: error }); +const notifyBatchOfUsersError = (error: Error) => { + return new Error(`could not notify the batch of users. Error ${error}`); }; const notifyBatchOfUsers = async (appName: string, appRequests: AppRequest[]): Promise => { From ae832af5fabdef1d72955b6e10f29958cc863349 Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Fri, 30 Dec 2022 13:47:42 -0300 Subject: [PATCH 13/19] ref: extract app requests related cronjob to its own file --- .../meteor/app/apps/server/appRequestsCron.ts | 68 +++++++++++++++++++ apps/meteor/app/apps/server/cron.js | 52 -------------- apps/meteor/app/apps/server/index.ts | 1 + 3 files changed, 69 insertions(+), 52 deletions(-) create mode 100644 apps/meteor/app/apps/server/appRequestsCron.ts diff --git a/apps/meteor/app/apps/server/appRequestsCron.ts b/apps/meteor/app/apps/server/appRequestsCron.ts new file mode 100644 index 000000000000..5eb773fa9143 --- /dev/null +++ b/apps/meteor/app/apps/server/appRequestsCron.ts @@ -0,0 +1,68 @@ +import { Meteor } from 'meteor/meteor'; +import { HTTP } from 'meteor/http'; +import { SyncedCron } from 'meteor/littledata:synced-cron'; + +import { Apps } from './orchestrator'; +import { getWorkspaceAccessToken } from '../../cloud/server'; +import { appRequestNotififyForUsers } from './marketplace/appRequestNotifyUsers'; + +export const appsNotifyAppRequests = Meteor.bindEnvironment(function _appsNotifyAppRequests() { + try { + const installedApps = Promise.await(Apps.installedApps({ enabled: true })); + if (!installedApps || installedApps.length === 0) { + return; + } + + const token = Promise.await(getWorkspaceAccessToken()); + const baseUrl = Apps.getMarketplaceUrl(); + if (!baseUrl) { + Apps.debugLog(`could not load marketplace base url to send app requests notifications`); + return; + } + + const options = { + headers: { + Authorization: `Bearer ${token}`, + }, + }; + + const pendingSentUrl = `${baseUrl}/v1/app-request/sent/pending`; + const result = HTTP.get(pendingSentUrl, options); + const data = result.data?.data; + const filtered = installedApps.filter((app) => data.indexOf(app.getID()) !== -1); + + filtered.forEach((app) => { + const appId = app.getID(); + const appName = app.getName(); + + const usersNotified = Promise.await<(string | Error)[]>( + appRequestNotififyForUsers(baseUrl, appId, appName) + .then((response) => { + // Mark all app requests as sent + HTTP.post(`${baseUrl}/v1/app-request/markAsSent/${appId}`, options); + return response; + }) + .catch((err) => { + Apps.debugLog(`could not send app request notifications for app ${appId}. Error: ${err}`); + return err; + }), + ); + + const errors = usersNotified.filter((batch) => batch instanceof Error); + if (errors.length > 0) { + Apps.debugLog(`Some batches of users could not be notified for app ${appId}. Errors: ${errors}`); + } + }); + } catch (err) { + Apps.debugLog(err); + } +}); + +// Scheduling as every 24 hours to avoid multiple instances hiting the marketplace at the same time +SyncedCron.add({ + name: 'Apps-Request-End-Users:notify', + schedule: (parser) => parser.text('every 24 hours'), + job() { + appsNotifyAppRequests(); + }, +}); diff --git a/apps/meteor/app/apps/server/cron.js b/apps/meteor/app/apps/server/cron.js index 48ce8dd00aa1..86a0e74b937e 100644 --- a/apps/meteor/app/apps/server/cron.js +++ b/apps/meteor/app/apps/server/cron.js @@ -9,7 +9,6 @@ import { Apps } from './orchestrator'; import { getWorkspaceAccessToken } from '../../cloud/server'; import { Users } from '../../models/server'; import { sendMessagesToAdmins } from '../../../server/lib/sendMessagesToAdmins'; -import { appRequestNotififyForUsers } from './marketplace/appRequestNotifyUsers'; const notifyAdminsAboutInvalidApps = Meteor.bindEnvironment(function _notifyAdminsAboutInvalidApps(apps) { if (!apps) { @@ -104,57 +103,6 @@ export const appsUpdateMarketplaceInfo = Meteor.bindEnvironment(function _appsUp Promise.await(Apps.updateAppsMarketplaceInfo(data).then(notifyAdminsAboutInvalidApps).then(notifyAdminsAboutRenewedApps)); }); -export const appsNotifyAppRequests = Meteor.bindEnvironment(function _appsNotifyAppRequests() { - try { - const installedApps = Promise.await(Apps.installedApps({ enabled: true })); - if (!installedApps || installedApps.length === 0) { - return; - } - - const token = Promise.await(getWorkspaceAccessToken()); - const baseUrl = Apps.getMarketplaceUrl(); - const options = { - headers: { - Authorization: `Bearer ${token}`, - }, - }; - - const pendingSentUrl = `${baseUrl}/v1/app-request/sent/pending`; - const result = HTTP.get(pendingSentUrl, options); - const data = result.data?.data; - const filtered = installedApps.filter((app) => data.indexOf(app.getID()) !== -1); - - filtered.forEach((app) => { - const appId = app.getID(); - const appName = app.getName(); - - const usersNotified = Promise.await( - appRequestNotififyForUsers(baseUrl, appId, appName) - .then(() => HTTP.post(`${baseUrl}/v1/app-request/markAsSent/${appId}`, options)) - .catch((err) => { - Apps.debugLog(`could not send app request notifications for app ${appId}. Error: ${err}`); - }), - ); - - const errors = usersNotified.filter((batch) => batch instanceof Error); - if (errors.length > 0) { - Apps.debugLog(`Some batches of users could not be notified for app ${appId}. Errors: ${errors}`); - } - }); - } catch (err) { - Apps.debugLog(err); - } -}); - -// Scheduling as every 24 hours to avoid multiple instances hiting the marketplace at the same time -SyncedCron.add({ - name: 'Apps-Request-End-Users:notify', - schedule: (parser) => parser.text('every 24 hours'), - job() { - appsNotifyAppRequests(); - }, -}); - SyncedCron.add({ name: 'Apps-Engine:check', schedule: (parser) => parser.text('at 4:00 am'), diff --git a/apps/meteor/app/apps/server/index.ts b/apps/meteor/app/apps/server/index.ts index ad3096af3158..35f7c2cc041f 100644 --- a/apps/meteor/app/apps/server/index.ts +++ b/apps/meteor/app/apps/server/index.ts @@ -1,3 +1,4 @@ import './cron'; +import './appRequestsCron'; export { Apps, AppEvents } from './orchestrator'; From 74b55f27708a82e332fb7ced90dbd874e314ae18 Mon Sep 17 00:00:00 2001 From: Matheus Carmo Date: Fri, 30 Dec 2022 16:28:49 -0300 Subject: [PATCH 14/19] feat: add learn more url on rocket.cat notification --- apps/meteor/app/apps/server/appRequestsCron.ts | 4 +++- .../apps/server/marketplace/appRequestNotifyUsers.ts | 10 ++++++---- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/apps/server/appRequestsCron.ts b/apps/meteor/app/apps/server/appRequestsCron.ts index 5eb773fa9143..ad5cc7fa99bf 100644 --- a/apps/meteor/app/apps/server/appRequestsCron.ts +++ b/apps/meteor/app/apps/server/appRequestsCron.ts @@ -2,6 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; import { SyncedCron } from 'meteor/littledata:synced-cron'; +import { settings } from '../../settings/server'; import { Apps } from './orchestrator'; import { getWorkspaceAccessToken } from '../../cloud/server'; import { appRequestNotififyForUsers } from './marketplace/appRequestNotifyUsers'; @@ -13,6 +14,7 @@ export const appsNotifyAppRequests = Meteor.bindEnvironment(function _appsNotify return; } + const workspaceUrl = settings.get('Site_Url'); const token = Promise.await(getWorkspaceAccessToken()); const baseUrl = Apps.getMarketplaceUrl(); if (!baseUrl) { @@ -36,7 +38,7 @@ export const appsNotifyAppRequests = Meteor.bindEnvironment(function _appsNotify const appName = app.getName(); const usersNotified = Promise.await<(string | Error)[]>( - appRequestNotififyForUsers(baseUrl, appId, appName) + appRequestNotififyForUsers(baseUrl, workspaceUrl, appId, appName) .then((response) => { // Mark all app requests as sent HTTP.post(`${baseUrl}/v1/app-request/markAsSent/${appId}`, options); diff --git a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts index 2d503bc74fa7..b5589bd07502 100644 --- a/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts +++ b/apps/meteor/app/apps/server/marketplace/appRequestNotifyUsers.ts @@ -13,7 +13,7 @@ const notifyBatchOfUsersError = (error: Error) => { return new Error(`could not notify the batch of users. Error ${error}`); }; -const notifyBatchOfUsers = async (appName: string, appRequests: AppRequest[]): Promise => { +const notifyBatchOfUsers = async (appName: string, learnMoreUrl: string, appRequests: AppRequest[]): Promise => { const batchRequesters = appRequests.reduce((acc: string[], appRequest: AppRequest) => { // Prevent duplicate requesters if (!acc.includes(appRequest.requester.id)) { @@ -25,7 +25,7 @@ const notifyBatchOfUsers = async (appName: string, appRequests: AppRequest[]): P const msgFn = (user: IUser): string => { const defaultLang = user.language || 'en'; - const msg = `${TAPi18n.__('App_request_enduser_message', { appname: appName, lng: defaultLang })}`; + const msg = `${TAPi18n.__('App_request_enduser_message', { appname: appName, learnmore: learnMoreUrl, lng: defaultLang })}`; return msg; }; @@ -39,6 +39,7 @@ const notifyBatchOfUsers = async (appName: string, appRequests: AppRequest[]): P export const appRequestNotififyForUsers = async ( marketplaceBaseUrl: string, + workspaceUrl: string, appId: string, appName: string, ): Promise<(string | Error)[]> => { @@ -67,11 +68,12 @@ export const appRequestNotififyForUsers = async ( // Calculate the number of loops - 1 because the first request was already made const loops = Math.ceil(total / pagination.limit) - 1; const requestsCollection = []; + const learnMore = `${workspaceUrl}admin/marketplace/all/info/${appId}`; // Notify first batch requestsCollection.push( Promise.resolve(appRequests.body.data.data.data) - .then((response) => notifyBatchOfUsers(appName, response)) + .then((response) => notifyBatchOfUsers(appName, learnMore, response)) .catch(notifyBatchOfUsersError), ); @@ -84,7 +86,7 @@ export const appRequestNotififyForUsers = async ( { headers }, ); - requestsCollection.push(notifyBatchOfUsers(appName, request.data.data)); + requestsCollection.push(notifyBatchOfUsers(appName, learnMore, request.data.data)); } const finalResult = await Promise.all(requestsCollection); diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 7a34656fd603..ad3619d044bf 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -5110,7 +5110,7 @@ "Version": "Version", "Version_version": "Version __version__", "App_version_incompatible_tooltip": "App incompatible with Rocket.Chat version", - "App_request_enduser_message": "The app you requested, __appname__ has just been installed on this workspace.", + "App_request_enduser_message": "The app you requested, __appname__ has just been installed on this workspace. [Click here to learn more](__learnmore__).", "Video_Conference_Description": "Configure conferencing calls for your workspace.", "Video_Chat_Window": "Video Chat", "Video_Conference": "Conference Call", From 5e7a480f39518fcfda74be35713f3973034e134d Mon Sep 17 00:00:00 2001 From: Bradley Hilton Date: Fri, 6 Jan 2023 13:43:28 -0600 Subject: [PATCH 15/19] Bump the time interval to 12 hours instead of 24 --- apps/meteor/app/apps/server/appRequestsCron.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/apps/server/appRequestsCron.ts b/apps/meteor/app/apps/server/appRequestsCron.ts index ad5cc7fa99bf..9709dc8c9bad 100644 --- a/apps/meteor/app/apps/server/appRequestsCron.ts +++ b/apps/meteor/app/apps/server/appRequestsCron.ts @@ -60,10 +60,10 @@ export const appsNotifyAppRequests = Meteor.bindEnvironment(function _appsNotify } }); -// Scheduling as every 24 hours to avoid multiple instances hiting the marketplace at the same time +// Scheduling as every 12 hours to avoid multiple instances hiting the marketplace at the same time SyncedCron.add({ name: 'Apps-Request-End-Users:notify', - schedule: (parser) => parser.text('every 24 hours'), + schedule: (parser) => parser.text('every 12 hours'), job() { appsNotifyAppRequests(); }, From 4fe611e71ec7f559611408d8b86e4d8a91f876f3 Mon Sep 17 00:00:00 2001 From: Bradley Hilton Date: Fri, 6 Jan 2023 13:47:03 -0600 Subject: [PATCH 16/19] Add default query parameters --- apps/meteor/app/apps/server/communication/rest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 54f8bf4e68a9..be432896c753 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -892,7 +892,7 @@ export class AppsRestApi { { async get() { const baseUrl = orchestrator.getMarketplaceUrl(); - const { appId, q, sort, limit, offset } = this.queryParams; + const { appId, q: '', sort: '', limit: 25, offset: 0 } = this.queryParams; const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); From 3ac1dcec050cb2392798569d498ffc6ec71be8b5 Mon Sep 17 00:00:00 2001 From: Bradley Hilton Date: Fri, 6 Jan 2023 13:47:58 -0600 Subject: [PATCH 17/19] Whoops, use the correct js syntax. --- apps/meteor/app/apps/server/communication/rest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index be432896c753..595f15e7d35f 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -892,7 +892,7 @@ export class AppsRestApi { { async get() { const baseUrl = orchestrator.getMarketplaceUrl(); - const { appId, q: '', sort: '', limit: 25, offset: 0 } = this.queryParams; + const { appId, q = '', sort = '', limit = 25, offset = 0 } = this.queryParams; const headers = getDefaultHeaders(); const token = await getWorkspaceAccessToken(); From 221d816b70aa2a42c09fdf90621824dc44be6376 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Wed, 11 Jan 2023 11:55:20 -0300 Subject: [PATCH 18/19] Prefer async function over `Promise` method chaining --- apps/meteor/client/views/admin/apps/AppMenu.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/views/admin/apps/AppMenu.js b/apps/meteor/client/views/admin/apps/AppMenu.js index bd23333d1ec6..bced835b5cfd 100644 --- a/apps/meteor/client/views/admin/apps/AppMenu.js +++ b/apps/meteor/client/views/admin/apps/AppMenu.js @@ -65,12 +65,12 @@ function AppMenu({ app, isAppDetailsPage, ...props }) { }, [setModal]); const confirmAction = useCallback( - (permissionsGranted) => { + async (permissionsGranted) => { setModal(null); - marketplaceActions[action]({ ...app, permissionsGranted }).then(async () => { - setLoading(false); - }); + await marketplaceActions[action]({ ...app, permissionsGranted }); + + setLoading(false); }, [setModal, action, app, setLoading], ); From 3c2b3de9bef24ee7b3dbf83935a0da0e6b15b1a4 Mon Sep 17 00:00:00 2001 From: Bradley Hilton Date: Wed, 11 Jan 2023 09:27:52 -0600 Subject: [PATCH 19/19] Switch to cursor from toArray --- apps/meteor/server/lib/sendDirectMessageToUsers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/lib/sendDirectMessageToUsers.ts b/apps/meteor/server/lib/sendDirectMessageToUsers.ts index c6a37eaf4451..9de055608998 100644 --- a/apps/meteor/server/lib/sendDirectMessageToUsers.ts +++ b/apps/meteor/server/lib/sendDirectMessageToUsers.ts @@ -15,7 +15,7 @@ export async function sendDirectMessageToUsers( throw new Error(`User not found: ${fromId}`); } - const users = await Users.findByIds(toIds, { projection: { _id: 1, username: 1, language: 1 } }).toArray(); + const users = Users.findByIds(toIds, { projection: { _id: 1, username: 1, language: 1 } }); const success: string[] = []; users.forEach((user: IUser) => {