UNPKG

@crowdin/app-project-module

Version:

Module that generates for you all common endpoints for serving standalone Crowdin App

458 lines (457 loc) 21.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.listenQueueMessage = exports.updateCrowdinFromWebhookRequest = exports.prepareWebhookData = exports.unregisterAllCrowdinWebhooks = exports.unregisterWebhooks = exports.registerWebhooks = exports.HookEvents = void 0; const logsFormatter = __importStar(require("@crowdin/logs-formatter")); const crowdinAppFunctions = __importStar(require("@crowdin/crowdin-apps-functions")); const amqplib_1 = __importDefault(require("amqplib")); const types_1 = require("../types"); const storage_1 = require("../../../storage"); const connection_1 = require("../../../util/connection"); const defaults_1 = require("./defaults"); const index_1 = require("../../../util/index"); const logger_1 = require("../../../util/logger"); const prefetchCount = 10; const forceProcessDelay = 10000; exports.HookEvents = { fileAdded: 'file.added', fileDeleted: 'file.deleted', fileApproved: 'file.approved', fileTranslated: 'file.translated', translationUpdated: 'translation.updated', }; const HookConditionEvents = { ALL: [exports.HookEvents.translationUpdated, exports.HookEvents.fileDeleted], TRANSLATED: [exports.HookEvents.fileTranslated, exports.HookEvents.fileDeleted], APPROVED: [exports.HookEvents.fileApproved, exports.HookEvents.fileDeleted], }; function encodedUrlParam({ config, crowdinContext, integration, }) { var _a; const params = { projectId: +crowdinContext.jwtPayload.context.project_id, clientId: crowdinContext.clientId, crowdinId: crowdinContext.crowdinId, }; const encryptedParams = (0, index_1.encryptData)(config, JSON.stringify(params)); return `${(_a = integration.webhooks) === null || _a === void 0 ? void 0 : _a.urlParam}=${encodeURIComponent(encryptedParams)}`; } function decodedUrlParam(config, data) { const params = (0, index_1.decryptData)(config, data); return JSON.parse(params); } function makeCrowdinWebhookUrl({ config, crowdinContext, integration, }) { var _a; const urlParam = encodedUrlParam({ config, integration, crowdinContext }); return (`${config.baseUrl}${((_a = integration.webhooks) === null || _a === void 0 ? void 0 : _a.crowdinWebhookUrl) ? integration.webhooks.crowdinWebhookUrl : '/api/crowdin/webhook'}` + `?${urlParam}`); } function registerWebhooks({ config, apiCredentials, appSettings, client, crowdinContext, integration, }) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { const isWebhookSync = Number(appSettings.schedule) !== types_1.SyncSchedule.DISABLED; const projectId = crowdinContext.jwtPayload.context.project_id; const urlParam = encodedUrlParam({ config, integration, crowdinContext }); if ((_a = integration.webhooks) === null || _a === void 0 ? void 0 : _a.crowdinWebhooks) { yield integration.webhooks.crowdinWebhooks(client, projectId, isWebhookSync, appSettings); } else { const webhookName = `${config.name} application hook ${crowdinContext.jwtPayload.sub}`; const webhookUrl = makeCrowdinWebhookUrl({ config, integration, crowdinContext, }); const syncCondition = types_1.SyncCondition[Number(appSettings.condition)]; const events = [...HookConditionEvents[syncCondition]]; const webhook = yield getCrowdinProjectWebhook({ client, projectId, name: webhookName, }); if (appSettings['new-crowdin-files']) { events.push(exports.HookEvents.fileAdded); } if (isWebhookSync && webhook) { yield updateCrowdinWebhooks({ client, projectId, webhook, events, }); } else if (isWebhookSync && !webhook) { yield registerCrowdinWebhook({ config, url: webhookUrl, client, crowdinContext, events, }); } else if (!isWebhookSync && webhook) { yield unregisterCrowdinWebhooks({ client, projectId, webhook }); } } if ((_b = integration.webhooks) === null || _b === void 0 ? void 0 : _b.integrationWebhooks) { yield integration.webhooks.integrationWebhooks(apiCredentials, urlParam, isWebhookSync, appSettings); } }); } exports.registerWebhooks = registerWebhooks; function unregisterWebhooks({ apiCredentials, appSettings, client, config, crowdinContext, integration, }) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { if ((_a = integration.webhooks) === null || _a === void 0 ? void 0 : _a.crowdinWebhooks) { yield integration.webhooks.crowdinWebhooks(client, crowdinContext.jwtPayload.context.project_id, false); } else { const webhookName = `${config.name} application hook ${crowdinContext.jwtPayload.sub}`; const webhook = yield getCrowdinProjectWebhook({ client, projectId: crowdinContext.jwtPayload.context.project_id, name: webhookName, }); if (webhook) { yield unregisterCrowdinWebhooks({ client, projectId: crowdinContext.jwtPayload.context.project_id, webhook, }); } } if ((_b = integration.webhooks) === null || _b === void 0 ? void 0 : _b.integrationWebhooks) { const urlParam = encodedUrlParam({ config, integration, crowdinContext }); yield integration.webhooks.integrationWebhooks(apiCredentials, urlParam, false, appSettings); } }); } exports.unregisterWebhooks = unregisterWebhooks; function getCrowdinProjectWebhook({ client, name, projectId, }) { return __awaiter(this, void 0, void 0, function* () { const hooks = (yield client.webhooksApi.withFetchAll().listWebhooks(projectId)).data.map((e) => e.data); return hooks.find((h) => h.name === name); }); } function getAllCrowdinProjectWebhooks({ client, config, projectId, }) { return __awaiter(this, void 0, void 0, function* () { const hooks = (yield client.webhooksApi.withFetchAll().listWebhooks(projectId)).data.map((e) => e.data); return hooks.filter((h) => h.name.startsWith(`${config.name} application hook `)); }); } function createPayload(events) { const payload = {}; for (const event of events) { payload[event] = { event: '{{event}}', projectId: '{{projectId}}', language: '{{targetLanguageId}}', fileId: '{{fileId}}', }; } return payload; } function registerCrowdinWebhook({ client, config, crowdinContext, events, url, }) { return __awaiter(this, void 0, void 0, function* () { const name = `${config.name} application hook ${crowdinContext.jwtPayload.sub}`; const payload = createPayload(events); yield client.webhooksApi.addWebhook(crowdinContext.jwtPayload.context.project_id, { name, url, events, requestType: 'POST', batchingEnabled: true, payload, }); }); } function updateCrowdinWebhooks({ client, events, projectId, webhook, }) { return __awaiter(this, void 0, void 0, function* () { const payload = createPayload(events); yield client.webhooksApi.editWebhook(projectId, webhook.id, [ { value: events, op: 'replace', path: '/events', }, { value: payload, op: 'replace', path: '/payload', }, ]); }); } function unregisterCrowdinWebhooks({ client, projectId, webhook, }) { return __awaiter(this, void 0, void 0, function* () { yield client.webhooksApi.deleteWebhook(projectId, webhook.id); }); } function unregisterAllCrowdinWebhooks({ config, crowdinId, integration, }) { return __awaiter(this, void 0, void 0, function* () { if (integration.webhooks) { const crowdinCredentials = yield (0, storage_1.getStorage)().getCrowdinCredentials(crowdinId); if (crowdinCredentials) { const credentials = yield (0, storage_1.getStorage)().getAllIntegrationCredentials(crowdinId); const projectIds = credentials.map((c) => crowdinAppFunctions.getProjectId(c.id)); const crowdinClient = yield (0, connection_1.prepareCrowdinClient)({ config, credentials: crowdinCredentials }); yield Promise.all(projectIds.map((projectId) => __awaiter(this, void 0, void 0, function* () { const webhooks = yield getAllCrowdinProjectWebhooks({ config, client: crowdinClient.client, projectId, }); yield Promise.all(webhooks.map((hook) => __awaiter(this, void 0, void 0, function* () { return yield unregisterCrowdinWebhooks({ client: crowdinClient.client, projectId, webhook: hook, }); }))); }))); } } }); } exports.unregisterAllCrowdinWebhooks = unregisterAllCrowdinWebhooks; function prepareWebhookData({ config, integration, provider, webhookUrlParam, }) { return __awaiter(this, void 0, void 0, function* () { let rootFolder = undefined; let syncSettings = null; let crowdinClient = null; let preparedIntegrationCredentials = null; let appSettings = {}; const { projectId, crowdinId, clientId } = decodedUrlParam(config, webhookUrlParam); const crowdinCredentials = yield (0, storage_1.getStorage)().getCrowdinCredentials(crowdinId); const integrationCredentials = yield (0, storage_1.getStorage)().getIntegrationCredentials(clientId); const integrationConfig = yield (0, storage_1.getStorage)().getIntegrationConfig(clientId); if (!crowdinCredentials) { return { projectId, crowdinClient, rootFolder, appSettings, syncSettings, preparedIntegrationCredentials }; } const context = Object.assign({ jwtPayload: { context: { // eslint-disable-next-line @typescript-eslint/camelcase project_id: projectId, // eslint-disable-next-line @typescript-eslint/camelcase organization_id: crowdinCredentials === null || crowdinCredentials === void 0 ? void 0 : crowdinCredentials.organizationId, // eslint-disable-next-line @typescript-eslint/camelcase user_id: crowdinCredentials === null || crowdinCredentials === void 0 ? void 0 : crowdinCredentials.userId, }, }, crowdinId: crowdinCredentials.id }, ((integrationCredentials === null || integrationCredentials === void 0 ? void 0 : integrationCredentials.id) && { clientId: integrationCredentials.id })); logsFormatter.resetContext(); logsFormatter.setContext(context); crowdinClient = yield (0, connection_1.prepareCrowdinClient)({ config, credentials: crowdinCredentials, context, }); if (!integrationCredentials) { return { projectId, crowdinClient, rootFolder, appSettings, syncSettings, preparedIntegrationCredentials }; } preparedIntegrationCredentials = yield (0, connection_1.prepareIntegrationCredentials)(config, integration, integrationCredentials); if (integrationConfig === null || integrationConfig === void 0 ? void 0 : integrationConfig.config) { appSettings = JSON.parse(integrationConfig.config); const isWebhookSync = Number(appSettings.schedule) !== types_1.SyncSchedule.DISABLED; if (isWebhookSync) { syncSettings = (yield (0, storage_1.getStorage)().getSyncSettings(clientId, crowdinId, 'schedule', provider)); // We can get an error in getRootFolder when the project has been deleted but the webhooks have been created try { rootFolder = yield (0, defaults_1.getRootFolder)(config, integration, crowdinClient.client, projectId); } catch (e) { (0, logger_1.logError)(e); } } } return { projectId, crowdinClient, preparedIntegrationCredentials, rootFolder, appSettings, syncSettings, }; }); } exports.prepareWebhookData = prepareWebhookData; function updateCrowdinFromWebhookRequest(args) { var _a, _b; return __awaiter(this, void 0, void 0, function* () { const { integration, webhookData, req } = args; let filesToSync = []; const { projectId, crowdinClient, preparedIntegrationCredentials, rootFolder, appSettings, syncSettings } = webhookData; const syncFiles = (syncSettings === null || syncSettings === void 0 ? void 0 : syncSettings.files) ? JSON.parse(syncSettings.files) : []; if ((_a = integration.webhooks) === null || _a === void 0 ? void 0 : _a.integrationWebhookInterceptor) { filesToSync = yield ((_b = integration.webhooks) === null || _b === void 0 ? void 0 : _b.integrationWebhookInterceptor(projectId, crowdinClient.client, preparedIntegrationCredentials, rootFolder, appSettings, syncSettings, req)); } for (const file of filesToSync) { const fileNodeType = file.nodeType || file.node_type || '1'; if (fileNodeType !== '1') { continue; } const initFile = Object.assign({ id: file.id, name: file.name, sync: false, schedule: true, // eslint-disable-next-line @typescript-eslint/camelcase parent_id: file.parent_id || file.parentId, // eslint-disable-next-line @typescript-eslint/camelcase node_type: file.nodeType || file.node_type || '1' }, (file.type ? { type: file.type } : {})); if (syncSettings) { if (!syncFiles.find((obj) => obj.id === initFile.id)) { yield (0, storage_1.getStorage)().updateSyncSettings(JSON.stringify([...syncFiles, initFile]), syncSettings.integrationId, syncSettings.crowdinId, 'schedule', syncSettings.provider); } const webhook = yield (0, storage_1.getStorage)().getWebhooks(initFile.id, syncSettings.integrationId, syncSettings.crowdinId, syncSettings.provider); if (!webhook) { yield (0, storage_1.getStorage)().saveWebhooks(initFile.id, syncSettings.integrationId, syncSettings.crowdinId, syncSettings.provider); } } } }); } exports.updateCrowdinFromWebhookRequest = updateCrowdinFromWebhookRequest; function listenQueueMessage({ config, integration, queueName, queueUrl, }) { return __awaiter(this, void 0, void 0, function* () { try { const connection = yield amqplib_1.default.connect(queueUrl); connection.once('close', function () { setTimeout(() => { listenQueueMessage({ config, integration, queueUrl, queueName }); }, 3000); return; }); const channel = yield connection.createChannel(); if (channel) { yield channel.assertQueue(queueName, { durable: true }); yield channel.prefetch(prefetchCount); const onMessage = consumer({ channel, config, integration }); yield channel.consume(queueName, onMessage, { noAck: false }); } } catch (e) { setTimeout(() => { listenQueueMessage({ config, integration, queueUrl, queueName }); }, 3000); } }); } exports.listenQueueMessage = listenQueueMessage; function consumer({ channel, config, integration, }) { let messagesCounter = 0; let webhooksInfo = {}; let webhooksData = []; let timeoutId; const resetStateVariables = () => { messagesCounter = 0; webhooksInfo = {}; webhooksData = []; }; return function (msg) { var _a; return __awaiter(this, void 0, void 0, function* () { messagesCounter++; if (!msg) { return; } clearTimeout(timeoutId); try { const data = JSON.parse(msg.content.toString()); const urlParam = (_a = integration.webhooks) === null || _a === void 0 ? void 0 : _a.urlParam; const webhookUrlParam = data.query[urlParam]; const { clientId } = decodedUrlParam(config, webhookUrlParam); if (!webhooksInfo[clientId]) { webhooksInfo[clientId] = {}; webhooksInfo[clientId].data = [data]; webhooksInfo[clientId].integration = integration; webhooksData.push(prepareWebhookData({ config, integration, webhookUrlParam, provider: types_1.Provider.INTEGRATION, }).then((res) => { if (res) { webhooksInfo[clientId].webhookData = res; } })); } else { webhooksInfo[clientId].data.push(data); } if (messagesCounter < prefetchCount) { // if all messages are not received, wait 10 seconds to force process messages timeoutId = setTimeout(() => __awaiter(this, void 0, void 0, function* () { yield processMessages({ webhooksData, webhooksInfo, channel, msg, }); resetStateVariables(); }), forceProcessDelay); return; } clearTimeout(timeoutId); yield processMessages({ webhooksData, webhooksInfo, channel, msg, }); resetStateVariables(); } catch (e) { (0, logger_1.logError)(e); } }); }; } function processMessages({ channel, msg, webhooksData, webhooksInfo, }) { return __awaiter(this, void 0, void 0, function* () { try { yield Promise.all(webhooksData); for (const { data, integration, webhookData } of Object.values(webhooksInfo)) { if (webhookData && webhookData.crowdinClient) { yield updateCrowdinFromWebhookRequest({ integration: integration, webhookData: webhookData, req: data, }); } } channel.ack(msg, true); } catch (e) { (0, logger_1.logError)(e); channel.nack(msg, false, false); } }); }