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.
---