import type { IUser, IRoom, IMessage, IOutgoingIntegration, IIncomingIntegration, IIntegration, IIntegrationHistory, } from '@rocket.chat/core-typings'; import type { Logger } from '@rocket.chat/logger'; import type { serverFetch } from '@rocket.chat/server-fetch'; import { wrapExceptions } from '@rocket.chat/tools'; import { incomingLogger, outgoingLogger } from '../logger'; import type { IScriptClass, CompiledScript } from './definition'; import { updateHistory } from './updateHistory'; type OutgoingRequestBaseData = { token: IOutgoingIntegration['token']; bot: boolean; trigger_word: string; }; type OutgoingRequestSendMessageData = OutgoingRequestBaseData & { channel_id: string; channel_name: string; message_id: string; timestamp: Date; user_id: string; user_name: string; text: string; siteUrl: string; alias?: string; bot?: boolean; isEdited?: true; tmid?: string; }; type OutgoingRequestUploadedFileData = OutgoingRequestBaseData & { channel_id: string; channel_name: string; message_id: string; timestamp: Date; user_id: string; user_name: string; text: string; user: IUser; room: IRoom; message: IMessage; alias?: string; bot?: boolean; }; type OutgoingRequestRoomCreatedData = OutgoingRequestBaseData & { channel_id: string; channel_name: string; timestamp: Date; user_id: string; user_name: string; owner: IUser; room: IRoom; }; type OutgoingRequestRoomData = OutgoingRequestBaseData & { channel_id: string; channel_name: string; timestamp: Date; user_id: string; user_name: string; owner: IUser; room: IRoom; bot?: boolean; }; type OutgoingRequestUserCreatedData = OutgoingRequestBaseData & { timestamp: Date; user_id: string; user_name: string; user: IUser; bot?: boolean; }; export type OutgoingRequestData = | OutgoingRequestSendMessageData | OutgoingRequestUploadedFileData | OutgoingRequestRoomCreatedData | OutgoingRequestRoomData | OutgoingRequestUserCreatedData; type OutgoingRequest = { params: Record; method: 'POST'; url: string; data: OutgoingRequestData; auth: undefined; headers: Record; }; type OutgoingRequestFromScript = { url?: string; headers?: Record; method?: string; message?: { text?: string; channel?: string; attachments?: { color?: string; author_name?: string; author_link?: string; author_icon?: string; title?: string; title_link?: string; text?: string; fields?: { title?: string; value?: string; short?: boolean; }[]; image_url?: string; thumb_url?: string; }[]; }; auth?: string; data?: Record; }; type OutgoingRequestContext = { integration: IOutgoingIntegration; data: OutgoingRequestData; historyId: IIntegrationHistory['_id']; url: string; }; type ProcessedOutgoingRequest = OutgoingRequest | OutgoingRequestFromScript; type OutgoingResponseContext = { integration: IOutgoingIntegration; request: ProcessedOutgoingRequest; response: Awaited>; content: string; historyId: IIntegrationHistory['_id']; }; type IncomingIntegrationRequest = { url: { hash: string | null | undefined; search: string | null | undefined; query: Record; pathname: string | null | undefined; path: string | null | undefined; }; url_raw: string; url_params: Record; content: Record; content_raw: string; headers: Record; body: Record; user: Pick, '_id' | 'name' | 'username'>; }; export abstract class IntegrationScriptEngine { protected compiledScripts: Record; public get disabled(): boolean { return this.isDisabled(); } public get incoming(): IsIncoming { return this.isIncoming; } constructor(private isIncoming: IsIncoming) { this.compiledScripts = {}; } public integrationHasValidScript(integration: IIntegration): boolean { return Boolean(!this.disabled && integration.scriptEnabled && integration.scriptCompiled && integration.scriptCompiled.trim() !== ''); } // PrepareOutgoingRequest will execute a script to build the request object that will be used for the actual integration request // It may also return a message object to be sent to the room where the integration was triggered public async prepareOutgoingRequest({ integration, data, historyId, url }: OutgoingRequestContext): Promise { const request: OutgoingRequest = { params: {}, method: 'POST', url, data, auth: undefined, headers: { 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.0 Safari/537.36', }, }; if (!(await this.hasScriptAndMethod(integration, 'prepare_outgoing_request'))) { return request; } return this.executeOutgoingScript(integration, 'prepare_outgoing_request', { request }, historyId); } public async processOutgoingResponse({ integration, request, response, content, historyId, }: OutgoingResponseContext): Promise { if (!(await this.hasScriptAndMethod(integration, 'process_outgoing_response'))) { return; } const sandbox = { request, response: { error: null, status_code: response.status, content, content_raw: content, headers: Object.fromEntries(response.headers), }, }; const scriptResult = await this.executeOutgoingScript(integration, 'process_outgoing_response', sandbox, historyId); if (scriptResult === false) { return scriptResult; } if (scriptResult?.content) { return scriptResult.content; } } public async processIncomingRequest({ integration, request, }: { integration: IIncomingIntegration; request: IncomingIntegrationRequest; }): Promise { return this.executeIncomingScript(integration, 'process_incoming_request', { request }); } protected get logger(): ReturnType { if (this.isIncoming) { return incomingLogger; } return outgoingLogger; } protected async executeOutgoingScript( integration: IOutgoingIntegration, method: keyof IScriptClass, params: Record, historyId: IIntegrationHistory['_id'], ): Promise { if (this.disabled) { return; } const script = await wrapExceptions(() => this.getIntegrationScript(integration)).suppress((e: any) => updateHistory({ historyId, step: 'execute-script-getting-script', error: true, errorStack: e, }), ); if (!script) { return; } if (!script[method]) { this.logger.error(`Method "${method}" not found in the Integration "${integration.name}"`); await updateHistory({ historyId, step: `execute-script-no-method-${method}` }); return; } try { await updateHistory({ historyId, step: `execute-script-before-running-${method}` }); const result = await this.runScriptMethod({ integrationId: integration._id, script, method, params, }); this.logger.debug({ msg: `Script method "${method}" result of the Integration "${integration.name}" is:`, result, }); return result; } catch (err: any) { await updateHistory({ historyId, step: `execute-script-error-running-${method}`, error: true, errorStack: err.stack.replace(/^/gm, ' '), }); this.logger.error({ msg: 'Error running Script in the Integration', integration: integration.name, err, }); this.logger.debug({ msg: 'Error running Script in the Integration', integration: integration.name, script: integration.scriptCompiled, }); } } protected async executeIncomingScript( integration: IIncomingIntegration, method: keyof IScriptClass, params: Record, ): Promise { if (!this.integrationHasValidScript(integration)) { return; } const script = await wrapExceptions(() => this.getIntegrationScript(integration)).catch((e) => { this.logger.error(e); throw e; }); if (!script[method]) { this.logger.error(`Method "${method}" not found in the Integration "${integration.name}"`); return; } return wrapExceptions(() => this.runScriptMethod({ integrationId: integration._id, script, method, params, }), ).catch((err: any) => { this.logger.error({ msg: 'Error running Script in Trigger', integration: integration.name, script: integration.scriptCompiled, err, }); throw new Error('error-running-script'); }); } protected async hasScriptAndMethod(integration: IIntegration, method: keyof IScriptClass): Promise { const script = await this.getScriptSafely(integration); return typeof script?.[method] === 'function'; } protected async getScriptSafely(integration: IIntegration): Promise | undefined> { if (this.disabled || integration.scriptEnabled !== true || !integration.scriptCompiled || integration.scriptCompiled.trim() === '') { return; } return wrapExceptions(() => this.getIntegrationScript(integration)).suppress(); } protected abstract isDisabled(): boolean; protected abstract runScriptMethod({ integrationId, script, method, params, }: { integrationId: IIntegration['_id']; script: IScriptClass; method: keyof IScriptClass; params: Record; }): Promise; protected abstract getIntegrationScript(integration: IIntegration): Promise>; }