diff --git a/packages/backend/package.json b/packages/backend/package.json index 4421eb9b..f7e8e1c2 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -30,6 +30,7 @@ "@sentry/node": "^7.42.0", "@sentry/tracing": "^7.42.0", "@types/luxon": "^2.3.1", + "@types/xmlrpc": "^1.3.7", "ajv-formats": "^2.1.1", "axios": "0.24.0", "bcrypt": "^5.0.1", @@ -62,7 +63,8 @@ "pg": "^8.7.1", "php-serialize": "^4.0.2", "stripe": "^11.13.0", - "winston": "^3.7.1" + "winston": "^3.7.1", + "xmlrpc": "^1.3.2" }, "contributors": [ { diff --git a/packages/backend/src/apps/discord/dynamic-data/list-channels/index.ts b/packages/backend/src/apps/discord/dynamic-data/list-channels/index.ts index e7b6ace6..6ab6e468 100644 --- a/packages/backend/src/apps/discord/dynamic-data/list-channels/index.ts +++ b/packages/backend/src/apps/discord/dynamic-data/list-channels/index.ts @@ -19,8 +19,8 @@ export default { channels.data = response.data .filter((channel: IJSONObject) => { - // filter in text channels only - return channel.type === 0; + // filter in text channels and announcement channels only + return channel.type === 0 || channel.type === 5; }) .map((channel: IJSONObject) => { return { diff --git a/packages/backend/src/apps/google-sheets/actions/create-spreadsheet/index.ts b/packages/backend/src/apps/google-sheets/actions/create-spreadsheet/index.ts new file mode 100644 index 00000000..b072cb7a --- /dev/null +++ b/packages/backend/src/apps/google-sheets/actions/create-spreadsheet/index.ts @@ -0,0 +1,105 @@ +import defineAction from '../../../../helpers/define-action'; + +type THeaders = { + __id: string; + header: string; +}[]; + +export default defineAction({ + name: 'Create spreadsheet', + key: 'createSpreadsheet', + description: + 'Create a blank spreadsheet or duplicate an existing spreadsheet. Optionally, provide headers.', + arguments: [ + { + label: 'Title', + key: 'title', + type: 'string' as const, + required: true, + description: '', + variables: true, + }, + { + label: 'Spreadsheet to copy', + key: 'spreadsheetId', + type: 'dropdown' as const, + required: false, + description: 'Choose a spreadsheet to copy its data.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSpreadsheets', + }, + ], + }, + }, + { + label: 'Headers', + key: 'headers', + type: 'dynamic' as const, + required: false, + description: + 'These headers are ignored if "Spreadsheet to Copy" is selected.', + fields: [ + { + label: 'Header', + key: 'header', + type: 'string' as const, + required: true, + variables: true, + }, + ], + }, + ], + + async run($) { + if ($.step.parameters.spreadsheetId) { + const body = { name: $.step.parameters.title }; + + const { data } = await $.http.post( + `https://www.googleapis.com/drive/v3/files/${$.step.parameters.spreadsheetId}/copy`, + body + ); + + $.setActionItem({ + raw: data, + }); + } else { + const headers = $.step.parameters.headers as THeaders; + const values = headers.map((entry) => entry.header); + + const spreadsheetBody = { + properties: { + title: $.step.parameters.title, + }, + sheets: [ + { + data: [ + { + startRow: 0, + startColumn: 0, + rowData: [ + { + values: values.map((header) => ({ + userEnteredValue: { stringValue: header }, + })), + }, + ], + }, + ], + }, + ], + }; + + const { data } = await $.http.post('/v4/spreadsheets', spreadsheetBody); + + $.setActionItem({ + raw: data, + }); + } + }, +}); diff --git a/packages/backend/src/apps/google-sheets/actions/index.ts b/packages/backend/src/apps/google-sheets/actions/index.ts index 7b0c8d2d..40e375bb 100644 --- a/packages/backend/src/apps/google-sheets/actions/index.ts +++ b/packages/backend/src/apps/google-sheets/actions/index.ts @@ -1,3 +1,4 @@ +import createSpreadsheet from './create-spreadsheet'; import createSpreadsheetRow from './create-spreadsheet-row'; -export default [createSpreadsheetRow]; +export default [createSpreadsheet, createSpreadsheetRow]; diff --git a/packages/backend/src/apps/mattermost/actions/index.ts b/packages/backend/src/apps/mattermost/actions/index.ts new file mode 100644 index 00000000..d28a6432 --- /dev/null +++ b/packages/backend/src/apps/mattermost/actions/index.ts @@ -0,0 +1,3 @@ +import sendMessageToChannel from './send-a-message-to-channel'; + +export default [sendMessageToChannel]; diff --git a/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/index.ts b/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/index.ts new file mode 100644 index 00000000..39734a99 --- /dev/null +++ b/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/index.ts @@ -0,0 +1,42 @@ +import defineAction from '../../../../helpers/define-action'; +import postMessage from './post-message'; + +export default defineAction({ + name: 'Send a message to channel', + key: 'sendMessageToChannel', + description: 'Sends a message to a channel you specify.', + arguments: [ + { + label: 'Channel', + key: 'channel', + type: 'dropdown' as const, + required: true, + description: 'Pick a channel to send the message to.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listChannels', + }, + ], + }, + }, + { + label: 'Message text', + key: 'message', + type: 'string' as const, + required: true, + description: 'The content of your new message.', + variables: true, + }, + ], + + async run($) { + const message = await postMessage($); + + return message; + }, +}); diff --git a/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/post-message.ts b/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/post-message.ts new file mode 100644 index 00000000..86bb7586 --- /dev/null +++ b/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/post-message.ts @@ -0,0 +1,27 @@ +import { IGlobalVariable } from '@automatisch/types'; + +type TData = { + channel_id: string; + message: string; +}; + +const postMessage = async ($: IGlobalVariable) => { + const { parameters } = $.step; + const channel_id = parameters.channel as string; + const message = parameters.message as string; + + const data: TData = { + channel_id, + message, + }; + + const response = await $.http.post('/api/v4/posts', data); + + const actionData = { + raw: response?.data, + }; + + $.setActionItem(actionData); +}; + +export default postMessage; diff --git a/packages/backend/src/apps/mattermost/assets/favicon.svg b/packages/backend/src/apps/mattermost/assets/favicon.svg new file mode 100644 index 00000000..1d5bf91f --- /dev/null +++ b/packages/backend/src/apps/mattermost/assets/favicon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/mattermost/auth/generate-auth-url.ts b/packages/backend/src/apps/mattermost/auth/generate-auth-url.ts new file mode 100644 index 00000000..a497e54a --- /dev/null +++ b/packages/backend/src/apps/mattermost/auth/generate-auth-url.ts @@ -0,0 +1,18 @@ +import { IGlobalVariable } from '@automatisch/types'; +import { URL, URLSearchParams } from 'url'; +import getBaseUrl from '../common/get-base-url'; + +export default async function generateAuthUrl($: IGlobalVariable) { + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId as string, + redirect_uri: $.auth.data.oAuthRedirectUrl as string, + response_type: 'code', + }); + + const baseUrl = getBaseUrl($); + const path = `/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url: new URL(path, baseUrl).toString(), + }); +} diff --git a/packages/backend/src/apps/mattermost/auth/index.ts b/packages/backend/src/apps/mattermost/auth/index.ts new file mode 100644 index 00000000..853cf1eb --- /dev/null +++ b/packages/backend/src/apps/mattermost/auth/index.ts @@ -0,0 +1,57 @@ +import generateAuthUrl from './generate-auth-url'; +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string' as const, + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/mattermost/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Mattermost OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'instanceUrl', + label: 'Mattermost instance URL', + type: 'string' as const, + required: false, + readOnly: false, + value: null, + placeholder: null, + description: 'Your Mattermost instance URL', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client id', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client secret', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/mattermost/auth/is-still-verified.ts b/packages/backend/src/apps/mattermost/auth/is-still-verified.ts new file mode 100644 index 00000000..befb7694 --- /dev/null +++ b/packages/backend/src/apps/mattermost/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getCurrentUser from '../common/get-current-user'; + +const isStillVerified = async ($: IGlobalVariable) => { + const user = await getCurrentUser($); + return !!user.id; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/mattermost/auth/verify-credentials.ts b/packages/backend/src/apps/mattermost/auth/verify-credentials.ts new file mode 100644 index 00000000..da0538ea --- /dev/null +++ b/packages/backend/src/apps/mattermost/auth/verify-credentials.ts @@ -0,0 +1,44 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getCurrentUser from '../common/get-current-user'; + +const verifyCredentials = async ($: IGlobalVariable) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value as string; + const params = { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + redirect_uri: redirectUri, + }; + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', // This is not documented yet required + }; + const response = await $.http.post('/oauth/access_token', null, { + params, + headers, + }); + + const { + data: { access_token, refresh_token, scope, token_type }, + } = response; + + $.auth.data.accessToken = response.data.access_token; + + const currentUser = await getCurrentUser($); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + accessToken: access_token, + refreshToken: refresh_token, + scope: scope, + tokenType: token_type, + userId: currentUser.id, + screenName: currentUser.username, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/mattermost/common/add-auth-header.ts b/packages/backend/src/apps/mattermost/common/add-auth-header.ts new file mode 100644 index 00000000..edbff231 --- /dev/null +++ b/packages/backend/src/apps/mattermost/common/add-auth-header.ts @@ -0,0 +1,12 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + requestConfig.headers = requestConfig.headers || {}; + requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/mattermost/common/add-x-requested-with-header.ts b/packages/backend/src/apps/mattermost/common/add-x-requested-with-header.ts new file mode 100644 index 00000000..65d89643 --- /dev/null +++ b/packages/backend/src/apps/mattermost/common/add-x-requested-with-header.ts @@ -0,0 +1,11 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addXRequestedWithHeader: TBeforeRequest = ($, requestConfig) => { + // This is not documented yet required + // ref. https://forum.mattermost.com/t/solved-invalid-or-expired-session-please-login-again/6772 + requestConfig.headers = requestConfig.headers || {}; + requestConfig.headers['X-Requested-With'] = `XMLHttpRequest`; + return requestConfig; +}; + +export default addXRequestedWithHeader; diff --git a/packages/backend/src/apps/mattermost/common/get-base-url.ts b/packages/backend/src/apps/mattermost/common/get-base-url.ts new file mode 100644 index 00000000..77538eca --- /dev/null +++ b/packages/backend/src/apps/mattermost/common/get-base-url.ts @@ -0,0 +1,7 @@ +import { IGlobalVariable } from '@automatisch/types'; + +const getBaseUrl = ($: IGlobalVariable): string => { + return $.auth.data.instanceUrl as string; +}; + +export default getBaseUrl; diff --git a/packages/backend/src/apps/mattermost/common/get-current-user.ts b/packages/backend/src/apps/mattermost/common/get-current-user.ts new file mode 100644 index 00000000..c6f42624 --- /dev/null +++ b/packages/backend/src/apps/mattermost/common/get-current-user.ts @@ -0,0 +1,9 @@ +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; + +const getCurrentUser = async ($: IGlobalVariable): Promise => { + const response = await $.http.get('/api/v4/users/me'); + const currentUser = response.data; + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/mattermost/common/set-base-url.ts b/packages/backend/src/apps/mattermost/common/set-base-url.ts new file mode 100644 index 00000000..8f3aab56 --- /dev/null +++ b/packages/backend/src/apps/mattermost/common/set-base-url.ts @@ -0,0 +1,9 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const setBaseUrl: TBeforeRequest = ($, requestConfig) => { + requestConfig.baseURL = $.auth.data.instanceUrl as string; + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/mattermost/dynamic-data/index.ts b/packages/backend/src/apps/mattermost/dynamic-data/index.ts new file mode 100644 index 00000000..fae496fc --- /dev/null +++ b/packages/backend/src/apps/mattermost/dynamic-data/index.ts @@ -0,0 +1,3 @@ +import listChannels from './list-channels'; + +export default [listChannels]; diff --git a/packages/backend/src/apps/mattermost/dynamic-data/list-channels/index.ts b/packages/backend/src/apps/mattermost/dynamic-data/list-channels/index.ts new file mode 100644 index 00000000..35cde864 --- /dev/null +++ b/packages/backend/src/apps/mattermost/dynamic-data/list-channels/index.ts @@ -0,0 +1,36 @@ +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; + +type TChannel = { + id: string; + display_name: string; +}; + +type TResponse = { + data: TChannel[]; +}; + +export default { + name: 'List channels', + key: 'listChannels', + + async run($: IGlobalVariable) { + const channels: { + data: IJSONObject[]; + error: IJSONObject | null; + } = { + data: [], + error: null, + }; + + const response: TResponse = await $.http.get('/api/v4/users/me/channels'); // this endpoint will return only channels user joined, there is no endpoint to list all channels available for user + + for (const channel of response.data) { + channels.data.push({ + value: channel.id as string, + name: (channel.display_name as string) || (channel.id as string), // it's possible for channel to not have any name thus falling back to using id + }); + } + + return channels; + }, +}; diff --git a/packages/backend/src/apps/mattermost/index.d.ts b/packages/backend/src/apps/mattermost/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/mattermost/index.ts b/packages/backend/src/apps/mattermost/index.ts new file mode 100644 index 00000000..91a75b8e --- /dev/null +++ b/packages/backend/src/apps/mattermost/index.ts @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import addXRequestedWithHeader from './common/add-x-requested-with-header'; +import setBaseUrl from './common/set-base-url'; +import auth from './auth'; +import actions from './actions'; +import dynamicData from './dynamic-data'; + +export default defineApp({ + name: 'Mattermost', + key: 'mattermost', + iconUrl: '{BASE_URL}/apps/mattermost/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/mattermost/connection', + baseUrl: 'https://mattermost.com', + apiBaseUrl: '', // there is no cloud version of this app, user always need to provide address of own instance when creating connection + primaryColor: '4a154b', + supportsConnections: true, + beforeRequest: [setBaseUrl, addXRequestedWithHeader, addAuthHeader], + auth, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/odoo/actions/create-lead/index.ts b/packages/backend/src/apps/odoo/actions/create-lead/index.ts new file mode 100644 index 00000000..a5935000 --- /dev/null +++ b/packages/backend/src/apps/odoo/actions/create-lead/index.ts @@ -0,0 +1,103 @@ +import defineAction from '../../../../helpers/define-action'; +import { authenticate, asyncMethodCall } from '../../common/xmlrpc-client'; + +export default defineAction({ + name: 'Create Lead', + key: 'createLead', + description: '', + arguments: [ + { + label: 'Name', + key: 'name', + type: 'string' as const, + required: true, + description: 'Lead name', + variables: true, + }, + { + label: 'Type', + key: 'type', + type: 'dropdown' as const, + required: true, + variables: true, + options: [ + { + label: 'Lead', + value: 'lead' + }, + { + label: 'Opportunity', + value: 'opportunity' + } + ] + }, + { + label: "Email", + key: 'email', + type: 'string' as const, + required: false, + description: 'Email of lead contact', + variables: true, + }, + { + label: "Contact Name", + key: 'contactName', + type: 'string' as const, + required: false, + description: 'Name of lead contact', + variables: true + }, + { + label: 'Phone Number', + key: 'phoneNumber', + type: 'string' as const, + required: false, + description: 'Phone number of lead contact', + variables: true + }, + { + label: 'Mobile Number', + key: 'mobileNumber', + type: 'string' as const, + required: false, + description: 'Mobile number of lead contact', + variables: true + } + ], + + async run($) { + const uid = await authenticate($); + const id = await asyncMethodCall( + $, + { + method: 'execute_kw', + params: [ + $.auth.data.databaseName, + uid, + $.auth.data.apiKey, + 'crm.lead', + 'create', + [ + { + name: $.step.parameters.name, + type: $.step.parameters.type, + email_from: $.step.parameters.email, + contact_name: $.step.parameters.contactName, + phone: $.step.parameters.phoneNumber, + mobile: $.step.parameters.mobileNumber + } + ] + ], + path: 'object', + }, + ); + + $.setActionItem( + { + raw: { + id: id + } + } + ) + } +}); diff --git a/packages/backend/src/apps/odoo/actions/index.ts b/packages/backend/src/apps/odoo/actions/index.ts new file mode 100644 index 00000000..70a23831 --- /dev/null +++ b/packages/backend/src/apps/odoo/actions/index.ts @@ -0,0 +1,3 @@ +import createLead from './create-lead'; + +export default [createLead]; diff --git a/packages/backend/src/apps/odoo/assets/favicon.svg b/packages/backend/src/apps/odoo/assets/favicon.svg new file mode 100644 index 00000000..aeb5dd77 --- /dev/null +++ b/packages/backend/src/apps/odoo/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/odoo/auth/index.ts b/packages/backend/src/apps/odoo/auth/index.ts new file mode 100644 index 00000000..4a9b99e5 --- /dev/null +++ b/packages/backend/src/apps/odoo/auth/index.ts @@ -0,0 +1,65 @@ +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'host', + label: 'Host Name', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Host name of your Odoo Server', + clickToCopy: false, + }, + { + key: 'port', + label: 'Port', + type: 'string' as const, + required: true, + readOnly: false, + value: '443', + placeholder: null, + description: 'Port that the host is running on, defaults to 443 (HTTPS)', + clickToCopy: false, + }, + { + key: 'databaseName', + label: 'Database Name', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Name of your Odoo database', + clickToCopy: false, + }, + { + key: 'email', + label: 'Email Address', + type: 'string' as const, + requires: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Email Address of the account that will be interacting with the database', + clickToCopy: false + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'API Key for your Odoo account', + clickToCopy: false + } + ], + + verifyCredentials, + isStillVerified +}; diff --git a/packages/backend/src/apps/odoo/auth/is-still-verified.ts b/packages/backend/src/apps/odoo/auth/is-still-verified.ts new file mode 100644 index 00000000..f676c026 --- /dev/null +++ b/packages/backend/src/apps/odoo/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +import {IGlobalVariable} from '@automatisch/types'; +import verifyCredentials from './verify-credentials'; + +const isStillVerified = async ($: IGlobalVariable) => { + await verifyCredentials($); + return true; +} + +export default isStillVerified; diff --git a/packages/backend/src/apps/odoo/auth/verify-credentials.ts b/packages/backend/src/apps/odoo/auth/verify-credentials.ts new file mode 100644 index 00000000..64dfbeae --- /dev/null +++ b/packages/backend/src/apps/odoo/auth/verify-credentials.ts @@ -0,0 +1,16 @@ +import { IGlobalVariable } from '@automatisch/types'; +import { authenticate } from '../common/xmlrpc-client'; + +const verifyCredentials = async ($: IGlobalVariable) => { + try { + await authenticate($); + + await $.auth.set({ + screenName: `${$.auth.data.email} @ ${$.auth.data.databaseName} - ${$.auth.data.host}`, + }); + } catch (error) { + throw new Error('Failed while authorizing!'); + } +} + +export default verifyCredentials; diff --git a/packages/backend/src/apps/odoo/common/xmlrpc-client.ts b/packages/backend/src/apps/odoo/common/xmlrpc-client.ts new file mode 100644 index 00000000..a29dd374 --- /dev/null +++ b/packages/backend/src/apps/odoo/common/xmlrpc-client.ts @@ -0,0 +1,67 @@ +import { join } from 'node:path'; +import xmlrpc from 'xmlrpc'; +import { IGlobalVariable } from "@automatisch/types"; + +type AsyncMethodCallPayload = { + method: string; + params: any[]; + path?: string; +} + +export const asyncMethodCall = async ($: IGlobalVariable, { method, params, path }: AsyncMethodCallPayload): Promise => { + return new Promise( + (resolve, reject) => { + const client = getClient($, { path }); + + client.methodCall( + method, + params, + (error, response) => { + if (error != null) { + // something went wrong on the server side, display the error returned by Odoo + reject(error); + } + + resolve(response); + } + ) + } + ); +} + +export const getClient = ($: IGlobalVariable, { path = 'common' }) => { + const host = $.auth.data.host as string; + const port = Number($.auth.data.port as string); + + return xmlrpc.createClient( + { + host, + port, + path: join('/xmlrpc/2', path), + } + ); +} + +export const authenticate = async ($: IGlobalVariable) => { + const uid = await asyncMethodCall( + $, + { + method: 'authenticate', + params: [ + $.auth.data.databaseName, + $.auth.data.email, + $.auth.data.apiKey, + [] + ] + } + ); + + if (!Number.isInteger(uid)) { + // failed to authenticate + throw new Error( + 'Failed to connect to the Odoo server. Please, check the credentials!' + ); + } + + return uid; +} diff --git a/packages/backend/src/apps/odoo/index.d.ts b/packages/backend/src/apps/odoo/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/odoo/index.ts b/packages/backend/src/apps/odoo/index.ts new file mode 100644 index 00000000..3502708b --- /dev/null +++ b/packages/backend/src/apps/odoo/index.ts @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app'; +import auth from './auth'; +import actions from './actions'; + +export default defineApp({ + name: 'Odoo', + key: 'odoo', + iconUrl: '{BASE_URL}/apps/odoo/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/odoo/connection', + supportsConnections: true, + baseUrl: 'https://odoo.com', + apiBaseUrl: '', + primaryColor: '9c5789', + auth, + actions +}); diff --git a/packages/backend/src/apps/postgresql/actions/delete/index.ts b/packages/backend/src/apps/postgresql/actions/delete/index.ts index 518017d0..4a79477c 100644 --- a/packages/backend/src/apps/postgresql/actions/delete/index.ts +++ b/packages/backend/src/apps/postgresql/actions/delete/index.ts @@ -102,6 +102,8 @@ export default defineAction({ }) .del() as IJSONArray; + client.destroy(); + $.setActionItem({ raw: { rows: response diff --git a/packages/backend/src/apps/postgresql/actions/insert/index.ts b/packages/backend/src/apps/postgresql/actions/insert/index.ts index 5c0e8545..37471fb3 100644 --- a/packages/backend/src/apps/postgresql/actions/insert/index.ts +++ b/packages/backend/src/apps/postgresql/actions/insert/index.ts @@ -88,6 +88,8 @@ export default defineAction({ .returning('*') .insert(data) as IJSONObject; + client.destroy(); + $.setActionItem({ raw: response[0] as IJSONObject }); }, }); diff --git a/packages/backend/src/apps/postgresql/actions/sql-query/index.ts b/packages/backend/src/apps/postgresql/actions/sql-query/index.ts index e5145cc7..9cfd0e6f 100644 --- a/packages/backend/src/apps/postgresql/actions/sql-query/index.ts +++ b/packages/backend/src/apps/postgresql/actions/sql-query/index.ts @@ -46,6 +46,7 @@ export default defineAction({ const queryStatemnt = $.step.parameters.queryStatement; const { rows } = await client.raw(queryStatemnt); + client.destroy(); $.setActionItem({ raw: { diff --git a/packages/backend/src/apps/postgresql/actions/update/index.ts b/packages/backend/src/apps/postgresql/actions/update/index.ts index 9a43506e..798e3593 100644 --- a/packages/backend/src/apps/postgresql/actions/update/index.ts +++ b/packages/backend/src/apps/postgresql/actions/update/index.ts @@ -132,6 +132,8 @@ export default defineAction({ }) .update(data) as IJSONArray; + client.destroy(); + $.setActionItem({ raw: { rows: response diff --git a/packages/backend/src/apps/postgresql/auth/verify-credentials.ts b/packages/backend/src/apps/postgresql/auth/verify-credentials.ts index edccad88..4815fb45 100644 --- a/packages/backend/src/apps/postgresql/auth/verify-credentials.ts +++ b/packages/backend/src/apps/postgresql/auth/verify-credentials.ts @@ -5,6 +5,7 @@ import getClient from '../common/postgres-client'; const verifyCredentials = async ($: IGlobalVariable) => { const client = getClient($); const checkConnection = await client.raw('SELECT 1'); + client.destroy(); logger.debug(checkConnection); diff --git a/packages/backend/src/apps/twilio/triggers/receive-sms/fetch-messages.ts b/packages/backend/src/apps/twilio/triggers/receive-sms/fetch-messages.ts index f6c7228a..fdf8a488 100644 --- a/packages/backend/src/apps/twilio/triggers/receive-sms/fetch-messages.ts +++ b/packages/backend/src/apps/twilio/triggers/receive-sms/fetch-messages.ts @@ -11,8 +11,20 @@ const fetchMessages = async ($: IGlobalVariable) => { response = await $.http.get(requestPath); response.data.messages.forEach((message: IJSONObject) => { + const computedMessage = { + To: message.to, + Body: message.body, + From: message.from, + SmsSid: message.sid, + NumMedia: message.num_media, + SmsStatus: message.status, + AccountSid: message.account_sid, + ApiVersion: message.api_version, + NumSegments: message.num_segments, + }; + const dataItem = { - raw: message, + raw: computedMessage, meta: { internalId: message.date_sent as string, }, diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js index 59965de5..1af34b4f 100644 --- a/packages/docs/pages/.vitepress/config.js +++ b/packages/docs/pages/.vitepress/config.js @@ -142,6 +142,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/http-request/connection' }, ], }, + { + text: 'Mattermost', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/mattermost/actions' }, + { text: 'Connection', link: '/apps/mattermost/connection' }, + ], + }, { text: 'Notion', collapsible: true, @@ -160,6 +169,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/ntfy/connection' }, ], }, + { + text: 'Odoo', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/odoo/actions' }, + { text: 'Connection', link: '/apps/odoo/connection' }, + ], + }, { text: 'OpenAI', collapsible: true, diff --git a/packages/docs/pages/apps/github/triggers.md b/packages/docs/pages/apps/github/triggers.md index ea63b4ad..ec82d3b3 100644 --- a/packages/docs/pages/apps/github/triggers.md +++ b/packages/docs/pages/apps/github/triggers.md @@ -2,13 +2,13 @@ favicon: /favicons/github.svg items: - name: New issues - desc: Triggers when a new issue is created + desc: Triggers when a new issue is created. - name: New pull requests - desc: Triggers when a new pull request is created + desc: Triggers when a new pull request is created. - name: New stargazers - desc: Triggers when a user stars a repository + desc: Triggers when a user stars a repository. - name: New watchers - desc: Triggers when a user watches a repository + desc: Triggers when a user watches a repository. --- + + diff --git a/packages/docs/pages/apps/mattermost/connection.md b/packages/docs/pages/apps/mattermost/connection.md new file mode 100644 index 00000000..9fa6187c --- /dev/null +++ b/packages/docs/pages/apps/mattermost/connection.md @@ -0,0 +1,19 @@ +# Mattermost + +:::info +This page explains the steps you need to follow to set up the Mattermost +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the `/integrations/oauth2-apps/add` page of your Mattermost server to register a **new OAuth application**. + - You can find details about registering new Mattermost oAuth application at https://docs.mattermost.com/integrations/cloud-oauth-2-0-applications.html#register-your-application-in-mattermost. +2. Fill in the **Display Name** field. +3. Fill in the **Description** field. +4. Fill in the **Homepage** field. +5. Copy **OAuth Redirect URL** from Automatisch to the **Callback URLs** field on Mattermost page. +6. Click on the **Save** button at the end of the form on Mattermost page. +7. Copy the **Client ID** value from the following page to the `Client ID` field on Automatisch. +8. Copy the **Client Secret** value from the same page to the `Client Secret` field on Automatisch. +9. Click **Done** button on MAttermost page. +10. Click **Submit** button on Automatisch. +11. Congrats! Start using your new Mattermost connection within the flows. diff --git a/packages/docs/pages/apps/odoo/actions.md b/packages/docs/pages/apps/odoo/actions.md new file mode 100644 index 00000000..e22fdb84 --- /dev/null +++ b/packages/docs/pages/apps/odoo/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/odoo.svg +items: + - name: Create a lead or opportunity + desc: Creates a new CRM record as a lead or opportunity. +--- + + + + \ No newline at end of file diff --git a/packages/docs/pages/apps/odoo/connection.md b/packages/docs/pages/apps/odoo/connection.md new file mode 100644 index 00000000..49b0d270 --- /dev/null +++ b/packages/docs/pages/apps/odoo/connection.md @@ -0,0 +1,16 @@ +# Odoo + +:::info +This page explains the steps you need to follow to set up the Odoo +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +To create a connection, you need to supply the following information: + +1. Fill the **Host Name** field with the Odoo host. +1. Fill the **Port** field with the Odoo port. +1. Fill the **Database Name** field with the Odoo database. +1. Fill the **Email Address** field with the email address of the account that will be intereacting with the database. +1. Fill the **API Key** field with the API key for your Odoo account. + +Odoo's [API documentation](https://www.odoo.com/documentation/latest/developer/reference/external_api.html#api-keys) explains how to create API keys. diff --git a/packages/docs/pages/apps/postgresql/actions.md b/packages/docs/pages/apps/postgresql/actions.md index 21a8753d..e877edb6 100644 --- a/packages/docs/pages/apps/postgresql/actions.md +++ b/packages/docs/pages/apps/postgresql/actions.md @@ -8,7 +8,7 @@ items: - name: Delete desc: Delete rows found based on the given where clause entries. - name: SQL query - desc: Executes the given SQL statement.. + desc: Executes the given SQL statement. ---