Compare commits

...

59 Commits

Author SHA1 Message Date
Faruk AYDIN
067ec2eb9c Release v0.8.0 2023-08-02 15:07:27 +02:00
Faruk AYDIN
2d332b32d9 chore: Update version to 0.8.0 in Dockerfiles 2023-08-02 15:06:49 +02:00
Ömer Faruk Aydın
1d9ad2ba86 Merge pull request #1184 from automatisch/notion-find-database-item
feat(notion): add find database item action
2023-08-01 13:54:09 +02:00
Ömer Faruk Aydın
a28e2177f7 Merge pull request #1183 from automatisch/notion-create-page
feat(notion): add create page action
2023-08-01 13:44:47 +02:00
Ömer Faruk Aydın
18fe0df691 Merge pull request #1185 from automatisch/email-case-insensitive-login
fix(auth): allow login with case insensitive email
2023-07-31 17:05:01 +03:00
Ömer Faruk Aydın
8e21a06d99 Merge pull request #1186 from automatisch/gitlab-use-user-projects
fix(gitlab/list-projects): list projects the user has membership
2023-07-31 17:03:25 +03:00
Ali BARIN
2daf5473bb fix(gitlab/list-projects): list projects the user has membership 2023-07-31 16:00:27 +02:00
Ömer Faruk Aydın
928ff53adf Merge pull request #1187 from automatisch/fix-gitlab-github-names
fix: GitHub and GitLab app names
2023-07-31 16:54:20 +03:00
Faruk AYDIN
a71e95e6e5 fix: GitHub and GitLab app names 2023-07-31 15:47:06 +02:00
Ali BARIN
cb4a54b5cc fix(auth): allow login with case insensitive email 2023-07-30 14:59:16 +00:00
Rıdvan Akca
37e4524156 feat(notion): add find database item action 2023-07-27 14:39:57 +03:00
dependabot[bot]
9ac24ee051 chore(deps): bump word-wrap from 1.2.3 to 1.2.4
Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4.
- [Release notes](https://github.com/jonschlinkert/word-wrap/releases)
- [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4)

---
updated-dependencies:
- dependency-name: word-wrap
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-26 22:51:23 +02:00
Ömer Faruk Aydın
f28ccd559a Merge pull request #1177 from automatisch/notion-create-database-item
feat(notion): add create database item action
2023-07-25 14:04:08 +03:00
Ömer Faruk Aydın
8e84a93d8e Merge pull request #1166 from automatisch/create-worksheet
feat(google-sheets): add create worksheet action
2023-07-25 13:58:41 +03:00
Ömer Faruk Aydın
d871dec1b7 Merge pull request #1179 from automatisch/add-http-proxy-agent
fix(axios): incorporate http(s)-proxy-agents
2023-07-24 15:40:22 +03:00
Ömer Faruk Aydın
b133e1a197 Merge pull request #1176 from automatisch/compute-params
fix: allow colon while computing step parameters
2023-07-24 15:33:16 +03:00
Rıdvan Akca
9346a037b9 feat(notion): add create page action 2023-07-24 14:50:53 +03:00
Ali BARIN
89facbcddd fix(axios): incorporate http(s)-proxy-agents 2023-07-17 22:23:48 +00:00
Faruk AYDIN
53fef35638 fix: Allow colon while computing step parameters 2023-07-17 18:19:54 +02:00
Rıdvan Akca
bfe496a09b feat(notion): add create database item action 2023-07-17 16:23:00 +03:00
dependabot[bot]
ff774c2e8e chore(deps): bump semver from 5.7.1 to 5.7.2
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-14 14:31:07 +02:00
Ömer Faruk Aydın
08a6d1078c Merge pull request #1132 from automatisch/show-webhook-url-by-flag
feat: introduce singleton webhook URL
2023-07-13 12:02:49 +02:00
Ömer Faruk Aydın
e9bcc919bf Merge branch 'main' into show-webhook-url-by-flag 2023-07-13 11:58:19 +02:00
Faruk AYDIN
04f4693c85 fix: Fetch webhooks by flow id 2023-07-12 14:05:46 +02:00
Ömer Faruk Aydın
2a58a0a4c4 Merge pull request #1174 from automatisch/twilio-receive-sms-fix
fix(twilio): Receive SMS webhook payload
2023-07-12 11:24:14 +02:00
Faruk AYDIN
d911843648 fix(twilio): Receive SMS webhook payload 2023-07-11 17:50:31 +02:00
Ömer Faruk Aydın
c80d178410 Merge pull request #1170 from automatisch/mattermost-docs
docs(mattermost): Fix links of mattermost app
2023-07-05 14:33:19 +02:00
Faruk AYDIN
9fb4dca39b docs(mattermost): Fix links of mattermost app 2023-07-05 14:29:19 +02:00
Rıdvan Akca
0dd444d50b feat(google-sheets): add create worksheet action 2023-06-30 19:03:54 +03:00
dependabot[bot]
f3bf418997 chore(deps): bump fast-xml-parser from 4.2.4 to 4.2.5 (#1164)
Bumps [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) from 4.2.4 to 4.2.5.
- [Release notes](https://github.com/NaturalIntelligence/fast-xml-parser/releases)
- [Changelog](https://github.com/NaturalIntelligence/fast-xml-parser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/NaturalIntelligence/fast-xml-parser/compare/v4.2.4...v4.2.5)

---
updated-dependencies:
- dependency-name: fast-xml-parser
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-06-29 17:51:47 +02:00
KrzysztofDK
676027245f feat(mattermost): add auth and send message to channel action
* feat: mattermost integration

* post review adjustments
2023-06-29 16:45:57 +02:00
Ali BARIN
6c5039f1ba fix(odoo): add missing empty type file (#1165) 2023-06-29 16:03:44 +02:00
Jack Dane
807be59f25 feat(odoo): add auth and create lead action (#1143)
* Add Odoo App and Icon

* Add Auth for Odoo

* Authorise with API key, the password would also work, but we should encourage an API key.

* Odoo Verify Credentials method

* Add the xmlrpc dependency so the backend can communicate with Odoo's API.
* Add a port to the auth fields to establish a connection that might not be over HTTPS.

* Add still verified method

* Currently no need to keep uid, so remove it from the auth data.
* Await the callback from the xmlrpc method call to ensure we don't verify credentials before the callback has been executed.

* Add Odoo create-lead action

* Provide basic functionality to create a lead.

* Add Odoo type option

* Let the user decide if the lead should be a "lead" or "opportunity" in the create-lead action.

* Add documentation for Odoo app

* Follow project standards

* Change indents to 2 spaces
* Use single quotes instead of double

* Commonise the authentication method (DRY)

* Use latest for API doc link

* refactor(odoo): abstract and organize implementation

---------

Co-authored-by: Ali BARIN <ali.barin53@gmail.com>
2023-06-29 15:56:20 +02:00
AnimatedSwine37
8e9896ec2e fix(postgresql): close connections when done 2023-06-27 18:57:18 +02:00
Rıdvan Akca
110c2dbac8 docs: add missing dots in action/trigger descriptions 2023-06-26 12:05:42 +02:00
Rıdvan Akca
f55ec4bd8a fix(google-sheets): sort actions 2023-06-25 15:50:03 +02:00
AnimatedSwine37
06c9bf420e fix(discord): show announcement channels in selection 2023-06-25 13:31:47 +02:00
Rıdvan Akca
3c9bc53a79 feat(google-sheets): add create spreadsheet action 2023-06-23 20:29:12 +02:00
Ali BARIN
de7a35dfe9 feat: introduce singleton webhook URL 2023-06-22 08:51:43 +00:00
Ömer Faruk Aydın
92638c2e97 Merge pull request #1142 from automatisch/fix-twilio-receive-sms-test-run
fix(twilio/receive-sms): use phone number via phone number sid
2023-06-14 13:42:11 +02:00
Ali BARIN
63251e6a9a fix(twilio/receive-sms): use phone number via phone number sid 2023-06-14 13:27:49 +02:00
Ömer Faruk Aydın
59844c33fd Merge pull request #1140 from automatisch/notion-app
feat(notion): add auth and new DB items trigger
2023-06-14 13:24:59 +02:00
Faruk AYDIN
d36dd2ece1 docs: Use triggers link of Notion for available apps 2023-06-14 13:21:34 +02:00
Ömer Faruk Aydın
1fdb94739b Merge pull request #1141 from automatisch/fix-dynamic-data
fix: skip prior execution steps if no prior execution
2023-06-14 12:54:34 +02:00
Ali BARIN
8a18f4c44f fix: skip prior execution steps if no prior execution 2023-06-13 20:56:39 +00:00
Ali BARIN
c9c47c5519 docs(notion): add auth and new DB items trigger 2023-06-13 05:56:33 +00:00
Ali BARIN
6be8b55daa feat(notion): add auth and new DB items trigger 2023-06-13 05:56:33 +00:00
Ali BARIN
f2dc2f5530 feat: introduce CustomAutocomplete with variables 2023-06-12 14:06:13 +02:00
Ömer Faruk Aydın
42842e7aec Merge pull request #1130 from gh-kdk/feature/gitlab-integration-documentation
docs(gitlab): add connection and triggers
2023-06-12 12:31:43 +02:00
Faruk AYDIN
49d9f77d1b docs: Add GitLab to available apps 2023-06-12 12:31:07 +02:00
Faruk AYDIN
d06a89564f docs: Add GitLab favicon 2023-06-12 12:31:07 +02:00
Krzysztof Dukszta-Kwiatkowski
58a8510d49 feat: gitlab integration documentation 2023-06-12 12:31:07 +02:00
Ömer Faruk Aydın
8055d6555e Merge pull request #1139 from automatisch/update-flow-status-accordingly
fix: update flow.active when remote calls succeed
2023-06-12 11:48:25 +02:00
Ömer Faruk Aydın
39620d3510 Merge pull request #1135 from shehabghazy/postgres-docs
docs(postgres): add connection and actions
2023-06-12 11:15:11 +02:00
Faruk AYDIN
6d19711926 docs: Add PostgreSQL to available apps 2023-06-12 11:13:09 +02:00
Faruk AYDIN
0b362dd435 docs: Use postgresql key name instead of postgres 2023-06-12 11:11:39 +02:00
Shehab Ghazy
9485731e7d docs: Add PostgreSQL documentation 2023-06-12 11:08:17 +02:00
Ali BARIN
1449fb0f84 fix: update flow.active when remote calls succeed 2023-06-11 20:05:00 +00:00
Faruk AYDIN
e548dd49ca chore: Use paddle sandbox for all non-prod cloud envs 2023-06-08 19:38:00 +02:00
164 changed files with 3793 additions and 649 deletions

View File

@@ -4,7 +4,7 @@ WORKDIR /automatisch
RUN \
apk --no-cache add --virtual build-dependencies python3 build-base && \
yarn global add @automatisch/cli@0.7.1 --network-timeout 1000000 && \
yarn global add @automatisch/cli@0.8.0 --network-timeout 1000000 && \
rm -rf /usr/local/share/.cache/ && \
apk del build-dependencies

View File

@@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM automatischio/automatisch:0.7.1
FROM automatischio/automatisch:0.8.0
WORKDIR /automatisch
RUN apk add --no-cache openssl dos2unix

View File

@@ -2,7 +2,7 @@
"packages": [
"packages/*"
],
"version": "0.7.1",
"version": "0.8.0",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {

View File

@@ -1,6 +1,6 @@
{
"name": "@automatisch/backend",
"version": "0.7.1",
"version": "0.8.0",
"license": "See LICENSE file",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"scripts": {
@@ -22,7 +22,7 @@
"prebuild": "rm -rf ./dist"
},
"dependencies": {
"@automatisch/web": "^0.7.1",
"@automatisch/web": "^0.8.0",
"@bull-board/express": "^3.10.1",
"@graphql-tools/graphql-file-loader": "^7.3.4",
"@graphql-tools/load": "^7.5.2",
@@ -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",
@@ -49,6 +50,8 @@
"graphql-type-json": "^0.3.2",
"handlebars": "^4.7.7",
"http-errors": "~1.6.3",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.1",
"jsonwebtoken": "^9.0.0",
"knex": "^2.4.0",
"lodash.get": "^4.4.2",
@@ -62,7 +65,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": [
{
@@ -100,7 +104,7 @@
"url": "https://github.com/automatisch/automatisch/issues"
},
"devDependencies": {
"@automatisch/types": "^0.7.1",
"@automatisch/types": "^0.8.0",
"@types/bcrypt": "^5.0.0",
"@types/bull": "^3.15.8",
"@types/cors": "^2.8.12",

View File

@@ -20,7 +20,7 @@ export default defineAction({
type: 'dropdown' as const,
required: true,
description: 'Language to translate the text to.',
variables: false,
variables: true,
value: '',
options: [
{ label: 'Bulgarian', value: 'BG' },

View File

@@ -13,7 +13,7 @@ export default defineAction({
required: true,
value: null,
description: 'Delay for unit, e.g. minutes, hours, days, weeks.',
variables: false,
variables: true,
options: [
{
label: 'Minutes',

View File

@@ -11,7 +11,7 @@ export default defineAction({
type: 'dropdown' as const,
required: true,
description: 'Pick a channel to send the message to.',
variables: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',

View File

@@ -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 {

View File

@@ -11,7 +11,7 @@ export default defineAction({
key: 'repo',
type: 'dropdown' as const,
required: false,
variables: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',

View File

@@ -6,7 +6,7 @@ import actions from './actions';
import dynamicData from './dynamic-data';
export default defineApp({
name: 'Github',
name: 'GitHub',
key: 'github',
baseUrl: 'https://github.com',
apiBaseUrl: 'https://api.github.com',

View File

@@ -9,11 +9,11 @@ export default {
// ref:
// - https://docs.gitlab.com/ee/api/projects.html#list-all-projects
// - https://docs.gitlab.com/ee/api/rest/index.html#keyset-based-pagination
const firstPageRequest = $.http.get('/api/v4/projects', {
params: {
simple: true,
pagination: 'keyset',
membership: true,
order_by: 'id',
sort: 'asc',
},

View File

@@ -6,7 +6,7 @@ import triggers from './triggers';
import dynamicData from './dynamic-data';
export default defineApp({
name: 'Gitlab',
name: 'GitLab',
key: 'gitlab',
baseUrl: 'https://gitlab.com',
apiBaseUrl: 'https://gitlab.com',

View File

@@ -21,7 +21,7 @@ export default defineAction({
required: false,
description:
'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.',
variables: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
@@ -40,7 +40,7 @@ export default defineAction({
required: true,
dependsOn: ['parameters.driveId'],
description: 'The spreadsheets in your Google Drive.',
variables: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
@@ -63,7 +63,7 @@ export default defineAction({
required: true,
dependsOn: ['parameters.spreadsheetId'],
description: 'The worksheets in your selected spreadsheet.',
variables: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',

View File

@@ -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,
});
}
},
});

View File

@@ -0,0 +1,191 @@
import { IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type THeaders = {
__id: string;
header: string;
}[];
type TSheetsResponse = {
sheets: {
properties: {
sheetId: string;
title: string;
};
}[];
};
type TBody = {
requests: IJSONObject[];
};
export default defineAction({
name: 'Create worksheet',
key: 'createWorksheet',
description:
'Create a blank worksheet with a title. Optionally, provide headers.',
arguments: [
{
label: 'Drive',
key: 'driveId',
type: 'dropdown' as const,
required: false,
description:
'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.',
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDrives',
},
],
},
},
{
label: 'Spreadsheet',
key: 'spreadsheetId',
type: 'dropdown' as const,
required: true,
dependsOn: ['parameters.driveId'],
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listSpreadsheets',
},
{
name: 'parameters.driveId',
value: '{parameters.driveId}',
},
],
},
},
{
label: 'Title',
key: 'title',
type: 'string' as const,
required: true,
description: '',
variables: true,
},
{
label: 'Headers',
key: 'headers',
type: 'dynamic' as const,
required: false,
fields: [
{
label: 'Header',
key: 'header',
type: 'string' as const,
required: true,
variables: true,
},
],
},
{
label: 'Overwrite',
key: 'overwrite',
type: 'dropdown' as const,
required: false,
value: false,
description:
'If a worksheet with the specified title exists, its content would be lost. Please, use with caution.',
variables: true,
options: [
{
label: 'Yes',
value: 'true',
},
{
label: 'No',
value: 'false',
},
],
},
],
async run($) {
const {
data: { sheets },
} = await $.http.get<TSheetsResponse>(
`/v4/spreadsheets/${$.step.parameters.spreadsheetId}`
);
const selectedSheet = sheets.find(
(sheet) => sheet.properties.title === $.step.parameters.title
);
const headers = $.step.parameters.headers as THeaders;
const values = headers.map((entry) => entry.header);
const body: TBody = {
requests: [
{
addSheet: {
properties: {
title: $.step.parameters.title,
},
},
},
],
};
if ($.step.parameters.overwrite === 'true' && selectedSheet) {
body.requests.unshift({
deleteSheet: {
sheetId: selectedSheet.properties.sheetId,
},
});
}
const { data } = await $.http.post(
`https://sheets.googleapis.com/v4/spreadsheets/${$.step.parameters.spreadsheetId}:batchUpdate`,
body
);
if (values.length) {
const body = {
requests: [
{
updateCells: {
rows: [
{
values: values.map((header) => ({
userEnteredValue: { stringValue: header },
})),
},
],
fields: '*',
start: {
sheetId:
data.replies[data.replies.length - 1].addSheet.properties
.sheetId,
rowIndex: 0,
columnIndex: 0,
},
},
},
],
};
const { data: response } = await $.http.post(
`https://sheets.googleapis.com/v4/spreadsheets/${$.step.parameters.spreadsheetId}:batchUpdate`,
body
);
$.setActionItem({
raw: response,
});
return;
}
$.setActionItem({
raw: data,
});
},
});

View File

@@ -1,3 +1,5 @@
import createSpreadsheet from './create-spreadsheet';
import createSpreadsheetRow from './create-spreadsheet-row';
import createWorksheet from './create-worksheet';
export default [createSpreadsheetRow];
export default [createSpreadsheet, createSpreadsheetRow, createWorksheet];

View File

@@ -84,7 +84,7 @@ export default defineAction({
type: 'string' as const,
required: true,
description: 'Header key',
variables: false,
variables: true,
},
{
label: 'Value',
@@ -132,7 +132,7 @@ export default defineAction({
throwIfFileSizeExceedsLimit(metadataResponse.headers['content-length']);
// eslint-disable-next-line no-empty
} catch {}
} catch { }
const requestData: AxiosRequestConfig = {
url,

View File

@@ -0,0 +1,3 @@
import sendMessageToChannel from './send-a-message-to-channel';
export default [sendMessageToChannel];

View File

@@ -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;
},
});

View File

@@ -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;

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="256px" viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid">
<g>
<path d="M6.79123171,86.9648684 C25.2351716,32.4823178 76.783459,-1.43234143 131.421839,0.0464773399 L131.421839,0.0464773399 L113.909757,20.739032 C81.4957329,26.5997669 53.5072568,48.7337413 42.5072761,81.2287969 C26.140539,129.576353 53.572705,182.545803 103.779303,199.543648 C153.985902,216.538377 207.952658,191.12264 224.319395,142.7782 C235.283535,110.390667 226.589826,75.9306053 204.563374,51.5978814 L204.563374,51.5978814 L203.21701,24.4290666 C247.371203,56.4768925 267.622761,114.633895 249.208429,169.029181 C226.546194,235.970273 153.909545,271.865521 86.9684532,249.204844 C20.0273609,226.542609 -15.8694453,153.905961 6.79123171,86.9648684 Z M165.185344,11.9237762 C165.839826,11.6401671 166.594039,11.5793938 167.321762,11.8256038 C168.035459,12.0671391 168.585536,12.5580009 168.936152,13.1595015 L168.936152,13.1595015 L169.007833,13.2763734 L169.071723,13.4103864 C169.240019,13.7313945 169.383381,14.0991514 169.450388,14.5510559 C169.582343,15.4417519 169.641535,17.5358595 169.665634,19.6808502 L169.671365,20.2662434 C169.677102,20.9486534 169.679633,21.6256073 169.680171,22.2599793 L169.680173,22.7924325 C169.678741,24.5267431 169.663874,25.8268542 169.663874,25.8268542 L169.663874,25.8268542 L170.167202,44.7600977 L170.910507,66.6151379 L171.837691,104.59538 C171.837691,104.59538 171.83785,104.602367 171.838064,104.616156 L171.838772,104.677745 C171.838883,104.691349 171.838983,104.706608 171.839058,104.723498 L171.839105,104.844231 C171.832023,107.013302 171.387173,122.892918 160.122454,133.928662 C148.009853,145.795053 133.131285,144.708923 123.451177,141.433394 C113.771069,138.154749 101.293828,129.979951 98.8800345,113.195592 C96.8283098,98.9302108 104.41287,86.9390787 106.734401,83.6627102 L106.889339,83.4459953 C107.205256,83.0081712 107.389865,82.7777388 107.389865,82.7777388 L107.389865,82.7777388 L131.197445,53.1717559 L145.064682,36.2627333 L156.965355,21.5275276 C156.965355,21.5275276 158.715313,19.1834331 160.51647,16.874806 L160.876881,16.4142586 C161.477025,15.6498178 162.070275,14.9069442 162.593713,14.2737698 L162.898895,13.907734 C163.342593,13.3805415 163.71955,12.9564826 163.983901,12.6998055 C164.292443,12.4006135 164.608776,12.205827 164.918876,12.0546727 L164.918876,12.0546727 L165.146386,11.9393591 Z" fill="#0058CC"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -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(),
});
}

View File

@@ -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,
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,7 @@
import { IGlobalVariable } from '@automatisch/types';
const getBaseUrl = ($: IGlobalVariable): string => {
return $.auth.data.instanceUrl as string;
};
export default getBaseUrl;

View File

@@ -0,0 +1,9 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
const getCurrentUser = async ($: IGlobalVariable): Promise<IJSONObject> => {
const response = await $.http.get('/api/v4/users/me');
const currentUser = response.data;
return currentUser;
};
export default getCurrentUser;

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
import listChannels from './list-channels';
export default [listChannels];

View File

@@ -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;
},
};

View File

View File

@@ -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,
});

View File

@@ -0,0 +1,100 @@
import { IJSONArray, IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type TBody = {
parent: IJSONObject;
properties: IJSONObject;
children: IJSONArray;
};
export default defineAction({
name: 'Create database item',
key: 'createDatabaseItem',
description: 'Creates an item in a database.',
arguments: [
{
label: 'Database',
key: 'databaseId',
type: 'dropdown' as const,
required: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDatabases',
},
],
},
},
{
label: 'Name',
key: 'name',
type: 'string' as const,
required: false,
description:
'This field has a 2000 character limit. Any characters beyond 2000 will not be included.',
variables: true,
},
{
label: 'Content',
key: 'content',
type: 'string' as const,
required: false,
description:
'The text to add to the page body. The max length for this field is 2000 characters. Any characters beyond 2000 will not be included.',
variables: true,
},
],
async run($) {
const name = $.step.parameters.name as string;
const truncatedName = name.slice(0, 2000);
const content = $.step.parameters.content as string;
const truncatedContent = content.slice(0, 2000);
const body: TBody = {
parent: {
database_id: $.step.parameters.databaseId,
},
properties: {},
children: [],
};
if (name) {
body.properties.Name = {
title: [
{
text: {
content: truncatedName,
},
},
],
};
}
if (content) {
body.children = [
{
object: 'block',
paragraph: {
rich_text: [
{
text: {
content: truncatedContent,
},
},
],
},
},
];
}
const { data } = await $.http.post('/v1/pages', body);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -0,0 +1,104 @@
import { IJSONArray, IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type TBody = {
parent: IJSONObject;
properties: IJSONObject;
children: IJSONArray;
};
export default defineAction({
name: 'Create page',
key: 'createPage',
description: 'Creates a page inside a parent page',
arguments: [
{
label: 'Parent page',
key: 'parentPageId',
type: 'dropdown' as const,
required: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listParentPages',
},
],
},
},
{
label: 'Title',
key: 'title',
type: 'string' as const,
required: false,
description:
'This field has a 2000 character limit. Any characters beyond 2000 will not be included.',
variables: true,
},
{
label: 'Content',
key: 'content',
type: 'string' as const,
required: false,
description:
'The text to add to the page body. The max length for this field is 2000 characters. Any characters beyond 2000 will not be included.',
variables: true,
},
],
async run($) {
const parentPageId = $.step.parameters.parentPageId as string;
const title = $.step.parameters.title as string;
const truncatedTitle = title.slice(0, 2000);
const content = $.step.parameters.content as string;
const truncatedContent = content.slice(0, 2000);
const body: TBody = {
parent: {
page_id: parentPageId,
},
properties: {},
children: [],
};
if (title) {
body.properties.title = {
type: 'title',
title: [
{
text: {
content: truncatedTitle,
},
},
],
};
}
if (content) {
body.children = [
{
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [
{
type: 'text',
text: {
content: truncatedContent,
},
},
],
},
},
];
}
const { data } = await $.http.post('/v1/pages', body);
$.setActionItem({
raw: data,
});
},
});

View File

@@ -0,0 +1,70 @@
import { IJSONArray, IJSONObject } from '@automatisch/types';
import defineAction from '../../../../helpers/define-action';
type TBody = {
filter: IJSONObject;
sorts: IJSONArray;
};
export default defineAction({
name: 'Find database item',
key: 'findDatabaseItem',
description: 'Searches for an item in a database by property.',
arguments: [
{
label: 'Database',
key: 'databaseId',
type: 'dropdown' as const,
required: true,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDatabases',
},
],
},
},
{
label: 'Name',
key: 'name',
type: 'string' as const,
required: false,
description:
'This field has a 2000 character limit. Any characters beyond 2000 will not be included.',
variables: true,
},
],
async run($) {
const databaseId = $.step.parameters.databaseId as string;
const name = $.step.parameters.name as string;
const truncatedName = name.slice(0, 2000);
const body: TBody = {
filter: {
property: 'Name',
rich_text: {
equals: truncatedName,
},
},
sorts: [
{
timestamp: 'last_edited_time',
direction: 'descending',
},
],
};
const { data } = await $.http.post(
`/v1/databases/${databaseId}/query`,
body
);
$.setActionItem({
raw: data.results[0],
});
},
});

View File

@@ -0,0 +1,5 @@
import createDatabaseItem from './create-database-item';
import createPage from './create-page';
import findDatabaseItem from './find-database-item';
export default [createDatabaseItem, createPage, findDatabaseItem];

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="256px" height="268px" viewBox="0 0 256 268" version="1.1" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid">
<g>
<path d="M16.0924984,11.5384656 L164.089991,0.608048392 C182.268719,-0.952166138 186.940447,0.0998642306 198.370133,8.40912104 L245.613429,41.6907258 C253.405586,47.4144843 256,48.9746988 256,55.2066414 L256,237.73391 C256,249.172512 251.845372,255.939385 237.304172,256.973584 L65.4398551,267.377986 C54.5272689,267.895086 49.3295257,266.334872 43.6146827,259.050899 L8.82635648,213.813593 C2.58549836,205.486505 0,199.254562 0,191.970589 L0,29.7261093 C0,20.3737376 4.1546284,12.572665 16.0924984,11.5384656 Z" fill="#FFFFFF"></path>
<path d="M164.089991,0.608048392 L16.0924984,11.5384656 C4.1546284,12.572665 0,20.3737376 0,29.7261093 L0,191.970589 C0,199.254562 2.58549836,205.486505 8.82635648,213.813593 L43.6146827,259.050899 C49.3295257,266.334872 54.5272689,267.895086 65.4398551,267.377986 L237.304172,256.973584 C251.836456,255.939385 256,249.172512 256,237.73391 L256,55.2066414 C256,49.2956572 253.664136,47.5927945 246.790277,42.5466149 C246.394749,42.2616979 245.999494,41.9764014 245.604513,41.6907258 L198.370133,8.40912104 C186.940447,0.0998642306 182.268719,-0.952166138 164.089991,0.608048392 Z M69.3270182,52.219945 C55.2940029,53.1649893 52.1111653,53.3789615 44.1406979,46.8973846 L23.8757401,30.7781396 C21.8162569,28.6919099 22.8504562,26.0885805 28.039284,25.5714809 L170.313018,15.1759943 C182.259804,14.1328795 188.482831,18.2964234 193.154559,21.9339521 L217.556314,39.6134116 C218.599429,40.1394268 221.193843,43.2509404 218.073414,43.2509404 L71.1457825,52.0951279 L69.3270182,52.219945 Z M52.9670544,236.173696 L52.9670544,81.2221043 C52.9670544,74.455231 55.0443686,71.3348019 61.2673957,70.8087867 L230.020199,60.9303999 C235.743958,60.4133002 238.329456,64.0508289 238.329456,70.8087867 L238.329456,224.726179 C238.329456,231.493052 237.286341,237.216811 227.942885,237.73391 L66.4562234,247.095198 C57.1127673,247.612297 52.9670544,244.500784 52.9670544,236.173696 Z M212.376402,89.5313611 C213.410601,94.2120046 212.376402,98.8926482 207.695758,99.4275789 L199.912517,100.969962 L199.912517,215.373807 C193.154559,219.011336 186.931532,221.08865 181.733788,221.08865 C173.424532,221.08865 171.347217,218.485321 165.12419,210.693164 L114.225535,130.614039 L114.225535,208.089834 L130.326949,211.736279 C130.326949,211.736279 130.326949,221.097566 117.337048,221.097566 L81.523438,223.17488 C80.4803232,221.08865 81.523438,215.890907 85.1520513,214.856708 L94.5044229,212.262294 L94.5044229,109.823065 L81.523438,108.771035 C80.4803232,104.090391 83.0747371,97.3324337 90.3497945,96.8064185 L128.77565,94.2209202 L181.733788,175.334245 L181.733788,103.573292 L168.235704,102.021993 C167.192589,96.2893189 171.347217,92.1257749 176.536045,91.6175908 L212.376402,89.5313611 L212.376402,89.5313611 Z" fill="#000000"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1,21 @@
import { IField, IGlobalVariable } from '@automatisch/types';
import { URL, URLSearchParams } from 'url';
export default async function generateAuthUrl($: IGlobalVariable) {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value as string;
const searchParams = new URLSearchParams({
client_id: $.auth.data.clientId as string,
redirect_uri: redirectUri,
response_type: 'code',
owner: 'user',
});
const url = new URL(`/v1/oauth/authorize?${searchParams}`, $.app.apiBaseUrl).toString();
await $.auth.set({
url,
});
}

View File

@@ -0,0 +1,49 @@
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/notion/connections/add',
placeholder: null,
description:
'When asked to input an OAuth callback or redirect URL in Notion OAuth, enter the URL above.',
docUrl: 'https://automatisch.io/docs/notion#oauth-redirect-url',
clickToCopy: true,
},
{
key: 'clientId',
label: 'Client ID',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/notion#client-id',
clickToCopy: false,
},
{
key: 'clientSecret',
label: 'Client Secret',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
docUrl: 'https://automatisch.io/docs/notion#client-secret',
clickToCopy: false,
},
],
generateAuthUrl,
verifyCredentials,
isStillVerified,
};

View File

@@ -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;

View File

@@ -0,0 +1,53 @@
import { IGlobalVariable, IField } from '@automatisch/types';
import getCurrentUser from '../common/get-current-user';
const verifyCredentials = async ($: IGlobalVariable) => {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value as string;
const response = await $.http.post(
`${$.app.apiBaseUrl}/v1/oauth/token`,
{
redirect_uri: redirectUri,
code: $.auth.data.code,
grant_type: 'authorization_code',
},
{
headers: {
Authorization: `Basic ${Buffer.from(
$.auth.data.clientId + ':' + $.auth.data.clientSecret
).toString('base64')}`,
},
additionalProperties: {
skipAddingAuthHeader: true
}
}
);
const data = response.data;
$.auth.data.accessToken = data.access_token;
await $.auth.set({
clientId: $.auth.data.clientId,
clientSecret: $.auth.data.clientSecret,
accessToken: data.access_token,
botId: data.bot_id,
duplicatedTemplateId: data.duplicated_template_id,
owner: data.owner,
tokenType: data.token_type,
workspaceIcon: data.workspace_icon,
workspaceId: data.workspace_id,
workspaceName: data.workspace_name,
screenName: data.workspace_name,
});
const currentUser = await getCurrentUser($);
await $.auth.set({
screenName: `${currentUser.name} @ ${data.workspace_name}`,
});
};
export default verifyCredentials;

View File

@@ -0,0 +1,14 @@
import { TBeforeRequest } from '@automatisch/types';
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
if (requestConfig.additionalProperties?.skipAddingAuthHeader) return requestConfig;
if ($.auth.data?.accessToken) {
const authorizationHeader = `Bearer ${$.auth.data.accessToken}`;
requestConfig.headers.Authorization = authorizationHeader;
}
return requestConfig;
};
export default addAuthHeader;

View File

@@ -0,0 +1,9 @@
import { TBeforeRequest } from '@automatisch/types';
const addNotionVersionHeader: TBeforeRequest = ($, requestConfig) => {
requestConfig.headers['Notion-Version'] = '2022-06-28';
return requestConfig;
};
export default addNotionVersionHeader;

View File

@@ -0,0 +1,17 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
type Owner = {
user: {
id: string
}
}
const getCurrentUser = async ($: IGlobalVariable): Promise<IJSONObject> => {
const userId = ($.auth.data.owner as Owner).user.id;
const response = await $.http.get(`/v1/users/${userId}`);
const currentUser = response.data;
return currentUser;
};
export default getCurrentUser;

View File

@@ -0,0 +1,4 @@
import listDatabases from './list-databases';
import listParentPages from './list-parent-pages';
export default [listDatabases, listParentPages];

View File

@@ -0,0 +1,60 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
type Database = {
id: string;
name: string;
title: [
{
plain_text: string;
}
];
}
type ResponseData = {
results: Database[];
next_cursor?: string;
}
type Payload = {
filter: {
value: 'database';
property: 'object';
};
start_cursor?: string;
};
export default {
name: 'List databases',
key: 'listDatabases',
async run($: IGlobalVariable) {
const databases: {
data: IJSONObject[];
error: IJSONObject | null;
} = {
data: [],
error: null,
};
const payload: Payload = {
filter: {
value: 'database',
property: 'object'
},
};
do {
const response = await $.http.post<ResponseData>('/v1/search', payload);
payload.start_cursor = response.data.next_cursor;
for (const database of response.data.results) {
databases.data.push({
value: database.id as string,
name: database.title[0].plain_text as string,
});
}
} while (payload.start_cursor);
return databases;
},
};

View File

@@ -0,0 +1,70 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
type Page = {
id: string;
properties: {
title: {
title: [
{
plain_text: string;
}
];
};
};
parent: {
workspace: boolean;
};
};
type ResponseData = {
results: Page[];
next_cursor?: string;
};
type Payload = {
filter: {
value: 'page';
property: 'object';
};
start_cursor?: string;
};
export default {
name: 'List parent pages',
key: 'listParentPages',
async run($: IGlobalVariable) {
const parentPages: {
data: IJSONObject[];
error: IJSONObject | null;
} = {
data: [],
error: null,
};
const payload: Payload = {
filter: {
value: 'page',
property: 'object',
},
};
do {
const response = await $.http.post<ResponseData>('/v1/search', payload);
payload.start_cursor = response.data.next_cursor;
const topLevelPages = response.data.results.filter(
(page) => page.parent.workspace
);
for (const pages of topLevelPages) {
parentPages.data.push({
value: pages.id as string,
name: pages.properties.title.title[0].plain_text as string,
});
}
} while (payload.start_cursor);
return parentPages;
},
};

View File

View File

@@ -0,0 +1,23 @@
import defineApp from '../../helpers/define-app';
import addAuthHeader from './common/add-auth-header';
import addNotionVersionHeader from './common/add-notion-version-header';
import auth from './auth';
import triggers from './triggers';
import actions from './actions';
import dynamicData from './dynamic-data';
export default defineApp({
name: 'Notion',
key: 'notion',
baseUrl: 'https://notion.com',
apiBaseUrl: 'https://api.notion.com',
iconUrl: '{BASE_URL}/apps/notion/assets/favicon.svg',
authDocUrl: 'https://automatisch.io/docs/apps/notion/connection',
primaryColor: '000000',
supportsConnections: true,
beforeRequest: [addAuthHeader, addNotionVersionHeader],
auth,
triggers,
actions,
dynamicData,
});

View File

@@ -0,0 +1,3 @@
import newDatabaseItems from './new-database-items';
export default [newDatabaseItems];

View File

@@ -0,0 +1,32 @@
import defineTrigger from '../../../../helpers/define-trigger';
import newDatabaseItems from './new-database-items';
export default defineTrigger({
name: 'New database items',
key: 'newDatabaseItems',
pollInterval: 15,
description: 'Triggers when a new database item is created',
arguments: [
{
label: 'Database',
key: 'databaseId',
type: 'dropdown' as const,
required: false,
variables: false,
source: {
type: 'query',
name: 'getDynamicData',
arguments: [
{
name: 'key',
value: 'listDatabases',
},
],
},
},
],
async run($) {
await newDatabaseItems($);
},
});

View File

@@ -0,0 +1,50 @@
import { IGlobalVariable } from '@automatisch/types';
type DatabaseItem = {
id: string;
}
type ResponseData = {
results: DatabaseItem[];
next_cursor?: string;
}
type Payload = {
sorts: [
{
timestamp: 'created_time' | 'last_edited_time';
direction: 'ascending' | 'descending';
}
];
start_cursor?: string;
};
const newDatabaseItems = async ($: IGlobalVariable) => {
const payload: Payload = {
sorts: [
{
timestamp: 'created_time',
direction: 'descending'
}
],
};
const databaseId = $.step.parameters.databaseId as string;
const path = `/v1/databases/${databaseId}/query`;
do {
const response = await $.http.post<ResponseData>(path, payload);
payload.start_cursor = response.data.next_cursor;
for (const databaseItem of response.data.results) {
$.pushTriggerItem({
raw: databaseItem,
meta: {
internalId: databaseItem.id,
}
})
}
} while (payload.start_cursor);
};
export default newDatabaseItems;

View File

@@ -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
}
}
)
}
});

View File

@@ -0,0 +1,3 @@
import createLead from './create-lead';
export default [createLead];

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="600" height="191"><circle cx="527.5" cy="118.4" r="72.4" fill="#888"/><path d="M527.5 161.1c23.6 0 42.7-19.1 42.7-42.7s-19.1-42.7-42.7-42.7-42.7 19.1-42.7 42.7 19.1 42.7 42.7 42.7z" fill="#fff"/><circle cx="374" cy="118.4" r="72.4" fill="#888"/><path d="M374 161.1c23.6 0 42.7-19.1 42.7-42.7S397.6 75.7 374 75.7s-42.7 19.1-42.7 42.7 19.1 42.7 42.7 42.7z" fill="#fff"/><path d="M294.9 117.8v.6c0 40-32.4 72.4-72.4 72.4s-72.4-32.4-72.4-72.4S182.5 46 222.5 46c16.4 0 31.5 5.5 43.7 14.6V14.4A14.34 14.34 0 0 1 280.6 0c7.9 0 14.4 6.5 14.4 14.4v102.7c0 .2 0 .5-.1.7z" fill="#888"/><circle cx="222.5" cy="118.4" r="42.7" fill="#fff"/><circle cx="72.4" cy="118.2" r="72.4" fill="#9c5789"/><circle cx="71.7" cy="118.5" r="42.7" fill="#fff"/><script xmlns=""/></svg>

After

Width:  |  Height:  |  Size: 803 B

View File

@@ -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
};

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 <T = number>($: IGlobalVariable, { method, params, path }: AsyncMethodCallPayload): Promise<T> => {
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;
}

View File

View File

@@ -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
});

View File

@@ -19,7 +19,7 @@ export default defineAction({
key: 'model',
type: 'dropdown' as const,
required: true,
variables: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
@@ -35,7 +35,7 @@ export default defineAction({
label: 'Messages',
key: 'messages',
type: 'dynamic' as const,
required: false,
required: true,
description: 'Add or remove messages as needed',
value: [{ role: 'system', body: '' }],
fields: [

View File

@@ -14,7 +14,7 @@ export default defineAction({
key: 'model',
type: 'dropdown' as const,
required: true,
variables: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',

View File

@@ -18,14 +18,14 @@ export default defineAction({
type: 'string' as const,
value: 'public',
required: true,
variables: false,
variables: true,
},
{
label: 'Table name',
key: 'table',
type: 'string' as const,
required: true,
variables: false,
variables: true,
},
{
label: 'Where clause entries',
@@ -38,14 +38,14 @@ export default defineAction({
key: 'columnName',
type: 'string' as const,
required: true,
variables: false,
variables: true,
},
{
label: 'Operator',
key: 'operator',
type: 'dropdown' as const,
required: true,
variables: false,
variables: true,
options: whereClauseOperators
},
{
@@ -69,7 +69,7 @@ export default defineAction({
key: 'parameter',
type: 'string' as const,
required: true,
variables: false,
variables: true,
},
{
label: 'Value',
@@ -102,6 +102,8 @@ export default defineAction({
})
.del() as IJSONArray;
client.destroy();
$.setActionItem({
raw: {
rows: response

View File

@@ -16,14 +16,14 @@ export default defineAction({
type: 'string' as const,
value: 'public',
required: true,
variables: false,
variables: true,
},
{
label: 'Table name',
key: 'table',
type: 'string' as const,
required: true,
variables: false,
variables: true,
},
{
label: 'Column - value entries',
@@ -37,7 +37,7 @@ export default defineAction({
key: 'columnName',
type: 'string' as const,
required: true,
variables: false,
variables: true,
},
{
label: 'Value',
@@ -52,7 +52,7 @@ export default defineAction({
label: 'Run-time parameters',
key: 'params',
type: 'dynamic' as const,
required: false,
required: true,
description: 'Change run-time configuration parameters with SET command',
fields: [
{
@@ -60,7 +60,7 @@ export default defineAction({
key: 'parameter',
type: 'string' as const,
required: true,
variables: false,
variables: true,
},
{
label: 'Value',
@@ -88,6 +88,8 @@ export default defineAction({
.returning('*')
.insert(data) as IJSONObject;
client.destroy();
$.setActionItem({ raw: response[0] as IJSONObject });
},
});

View File

@@ -27,7 +27,7 @@ export default defineAction({
key: 'parameter',
type: 'string' as const,
required: true,
variables: false,
variables: true,
},
{
label: 'Value',
@@ -46,6 +46,7 @@ export default defineAction({
const queryStatemnt = $.step.parameters.queryStatement;
const { rows } = await client.raw(queryStatemnt);
client.destroy();
$.setActionItem({
raw: {

View File

@@ -19,14 +19,14 @@ export default defineAction({
type: 'string' as const,
value: 'public',
required: true,
variables: false,
variables: true,
},
{
label: 'Table name',
key: 'table',
type: 'string' as const,
required: true,
variables: false,
variables: true,
},
{
label: 'Where clause entries',
@@ -39,14 +39,14 @@ export default defineAction({
key: 'columnName',
type: 'string' as const,
required: true,
variables: false,
variables: true,
},
{
label: 'Operator',
key: 'operator',
type: 'dropdown' as const,
required: true,
variables: false,
variables: true,
options: whereClauseOperators
},
{
@@ -70,7 +70,7 @@ export default defineAction({
key: 'columnName',
type: 'string' as const,
required: true,
variables: false,
variables: true,
},
{
label: 'Value',
@@ -93,7 +93,7 @@ export default defineAction({
key: 'parameter',
type: 'string' as const,
required: true,
variables: false,
variables: true,
},
{
label: 'Value',
@@ -132,6 +132,8 @@ export default defineAction({
})
.update(data) as IJSONArray;
client.destroy();
$.setActionItem({
raw: {
rows: response

View File

@@ -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);

View File

@@ -10,6 +10,7 @@ export default defineAction({
key: 'object',
type: 'dropdown' as const,
required: true,
variables: true,
description: 'Pick which type of object you want to search for.',
source: {
type: 'query',
@@ -28,7 +29,7 @@ export default defineAction({
type: 'dropdown' as const,
description: 'Pick which field to search by',
required: true,
variables: false,
variables: true,
dependsOn: ['parameters.object'],
source: {
type: 'query',

View File

@@ -23,7 +23,7 @@ export default defineAction({
'Sort messages by their match strength or by their date. Default is score.',
required: true,
value: 'score',
variables: false,
variables: true,
options: [
{
label: 'Match strength',
@@ -43,7 +43,7 @@ export default defineAction({
'Sort matching messages in ascending or descending order. Default is descending.',
required: true,
value: 'desc',
variables: false,
variables: true,
options: [
{
label: 'Descending (newest or best match first)',

View File

@@ -12,7 +12,7 @@ export default defineAction({
type: 'dropdown' as const,
required: true,
description: 'Pick a user to send the message to.',
variables: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
@@ -40,7 +40,7 @@ export default defineAction({
value: false,
description:
'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.',
variables: false,
variables: true,
options: [
{
label: 'Yes',

View File

@@ -12,7 +12,7 @@ export default defineAction({
type: 'dropdown' as const,
required: true,
description: 'Pick a channel to send the message to.',
variables: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
@@ -40,7 +40,7 @@ export default defineAction({
value: false,
description:
'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.',
variables: false,
variables: true,
options: [
{
label: 'Yes',

View File

@@ -29,7 +29,7 @@ export default defineAction({
required: false,
value: false,
description: 'Sends the message silently. Users will receive a notification with no sound.',
variables: false,
variables: true,
options: [
{
label: 'Yes',

View File

@@ -10,7 +10,7 @@ export default defineAction({
key: 'projectId',
type: 'dropdown' as const,
required: false,
variables: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',
@@ -27,7 +27,7 @@ export default defineAction({
key: 'sectionId',
type: 'dropdown' as const,
required: false,
variables: false,
variables: true,
dependsOn: ['parameters.projectId'],
source: {
type: 'query',

View File

@@ -13,7 +13,7 @@ export default defineAction({
required: true,
description:
'The number to send the SMS from. Include country code. Example: 15551234567',
variables: false,
variables: true,
source: {
type: 'query',
name: 'getDynamicData',

View File

@@ -0,0 +1,14 @@
import { IGlobalVariable } from "@automatisch/types";
type Response = {
sid: string;
phone_number: string;
};
export default async function getIncomingPhoneNumber($: IGlobalVariable) {
const phoneNumberSid = $.step.parameters.phoneNumberSid as string;
const path = `/2010-04-01/Accounts/${$.auth.data.accountSid}/IncomingPhoneNumbers/${phoneNumberSid}.json`;
const response = await $.http.get<Response>(path);
return response.data;
};

View File

@@ -1,17 +1,30 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import getIncomingPhoneNumber from '../../common/get-incoming-phone-number';
const fetchMessages = async ($: IGlobalVariable) => {
const toNumber = $.step.parameters.toNumber as string;
const incomingPhoneNumber = await getIncomingPhoneNumber($);
let response;
let requestPath = `/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages.json?To=${toNumber}`;
let requestPath = `/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages.json?To=${incomingPhoneNumber.phone_number}`;
do {
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,
},

View File

@@ -34,6 +34,9 @@ export default defineTrigger({
},
],
useSingletonWebhook: true,
singletonWebhookRefValueParameter: 'phoneNumberSid',
async testRun($) {
await fetchMessages($);

View File

@@ -5,6 +5,7 @@ export default defineTrigger({
name: 'Catch raw webhook',
key: 'catchRawWebhook',
type: 'webhook',
showWebhookUrl: true,
description: 'Triggers when the webhook receives a request.',
async testRun($) {

View File

@@ -0,0 +1,40 @@
import path from 'node:path';
import { Response } from 'express';
import { IRequest } from '@automatisch/types';
import Connection from '../../models/connection';
import logger from '../../helpers/logger';
import handler from '../../helpers/webhook-handler';
export default async (request: IRequest, response: Response) => {
const computedRequestPayload = {
headers: request.headers,
body: request.body,
query: request.query,
params: request.params,
};
logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`);
logger.debug(JSON.stringify(computedRequestPayload, null, 2));
const { connectionId } = request.params;
const connection = await Connection.query()
.findById(connectionId)
.throwIfNotFound();
if (!await connection.verifyWebhook(request)) {
return response.sendStatus(401);
}
const triggerSteps = await connection
.$relatedQuery('triggerSteps')
.where('webhook_path', path.join(request.baseUrl, request.path));
if (triggerSteps.length === 0) return response.sendStatus(404);
for (const triggerStep of triggerSteps) {
await handler(triggerStep.flowId, request, response);
}
response.sendStatus(204);
};

View File

@@ -0,0 +1,34 @@
import { Response } from 'express';
import { IRequest } from '@automatisch/types';
import Flow from '../../models/flow';
import logger from '../../helpers/logger';
import handler from '../../helpers/webhook-handler';
export default async (request: IRequest, response: Response) => {
const computedRequestPayload = {
headers: request.headers,
body: request.body,
query: request.query,
params: request.params,
};
logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`);
logger.debug(JSON.stringify(computedRequestPayload, null, 2));
const flowId = request.params.flowId;
const flow = await Flow.query().findById(flowId).throwIfNotFound();
const triggerStep = await flow.getTriggerStep();
if (triggerStep.appKey !== 'webhook') {
const connection = await triggerStep.$relatedQuery('connection');
if (!(await connection.verifyWebhook(request))) {
return response.sendStatus(401);
}
}
await handler(flowId, request, response);
response.sendStatus(204);
};

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return knex.schema.table('steps', (table) => {
table.string('webhook_path');
});
}
export async function down(knex: Knex): Promise<void> {
return knex.schema.table('steps', (table) => {
table.dropColumn('webhook_path');
});
}

View File

@@ -0,0 +1,16 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
return await knex('steps')
.where('type', 'trigger')
.whereIn('app_key', ['gitlab', 'typeform', 'twilio', 'flowers-software', 'webhook'])
.update({
webhook_path: knex.raw('? || ??', ['/webhooks/flows/', knex.ref('flow_id')]),
});
}
export async function down(knex: Knex): Promise<void> {
return await knex('steps').update({
webhook_path: null
});
}

View File

@@ -13,7 +13,7 @@ const TOKEN_EXPIRES_IN = '14d';
const login = async (_parent: unknown, params: Params) => {
const user = await User.query().findOne({
email: params.input.email,
email: params.input.email.toLowerCase(),
});
if (user && (await user.login(params.input.password))) {

View File

@@ -25,14 +25,12 @@ const updateFlowStatus = async (
})
.throwIfNotFound();
if (flow.active === params.input.active) {
const newActiveValue = params.input.active;
if (flow.active === newActiveValue) {
return flow;
}
flow = await flow.$query().withGraphFetched('steps').patchAndFetch({
active: params.input.active,
});
const triggerStep = await flow.getTriggerStep();
const trigger = await triggerStep.getTriggerCommand();
const interval = trigger.getInterval?.(triggerStep.parameters);
@@ -49,13 +47,13 @@ const updateFlowStatus = async (
testRun: false,
});
if (flow.active && trigger.registerHook) {
if (newActiveValue && trigger.registerHook) {
await trigger.registerHook($);
} else if (!flow.active && trigger.unregisterHook) {
} else if (!newActiveValue && trigger.unregisterHook) {
await trigger.unregisterHook($);
}
} else {
if (flow.active) {
if (newActiveValue) {
flow = await flow.$query().patchAndFetch({
published_at: new Date().toISOString(),
});
@@ -80,6 +78,10 @@ const updateFlowStatus = async (
}
}
flow = await flow.$query().withGraphFetched('steps').patchAndFetch({
active: newActiveValue,
});
return flow;
};

View File

@@ -60,6 +60,8 @@ const updateStep = async (
})
.withGraphFetched('connection');
await step.updateWebhookUrl();
return step;
};

View File

@@ -1,7 +1,9 @@
import { IDynamicData, IJSONObject } from '@automatisch/types';
import Context from '../../types/express/context';
import App from '../../models/app';
import ExecutionStep from '../../models/execution-step';
import globalVariable from '../../helpers/global-variable';
import computeParameters from '../../helpers/compute-parameters';
type Params = {
stepId: string;
@@ -28,18 +30,32 @@ const getDynamicData = async (
if (!connection || !step.appKey) return null;
const flow = step.flow;
const app = await App.findOneByKey(step.appKey);
const $ = await globalVariable({ connection, app, flow: step.flow, step });
const $ = await globalVariable({ connection, app, flow, step });
const command = app.dynamicData.find(
(data: IDynamicData) => data.key === params.key
);
// apply run-time parameters that're not persisted yet
for (const parameterKey in params.parameters) {
const parameterValue = params.parameters[parameterKey];
$.step.parameters[parameterKey] = parameterValue;
}
const lastExecution = await flow.$relatedQuery('lastExecution');
const lastExecutionId = lastExecution?.id;
const priorExecutionSteps = lastExecutionId ? await ExecutionStep.query().where({
execution_id: lastExecutionId,
}) : [];
// compute variables in parameters
const computedParameters = computeParameters($.step.parameters, priorExecutionSteps);
$.step.parameters = computedParameters;
const fetchedData = await command.run($);
if (fetchedData.error) {

View File

@@ -82,6 +82,7 @@ type Trigger {
name: String
key: String
description: String
showWebhookUrl: Boolean
pollInterval: Int
type: String
substeps: [Substep]

View File

@@ -0,0 +1,24 @@
import axios, { AxiosRequestConfig } from 'axios';
import { HttpsProxyAgent } from 'https-proxy-agent';
import { HttpProxyAgent } from 'http-proxy-agent';
const config: AxiosRequestConfig = {};
const httpProxyUrl = process.env.http_proxy;
const httpsProxyUrl = process.env.https_proxy;
const supportsProxy = httpProxyUrl || httpsProxyUrl;
if (supportsProxy) {
if (httpProxyUrl) {
config.httpAgent = new HttpProxyAgent(process.env.http_proxy);
}
if (httpsProxyUrl) {
config.httpsAgent = new HttpsProxyAgent(process.env.https_proxy);
}
config.proxy = false;
}
const axiosWithProxyInstance = axios.create(config);
export default axiosWithProxyInstance;

View File

@@ -4,7 +4,7 @@ import paddlePlans from './plans.ee';
import webhooks from './webhooks.ee';
const paddleInfo = {
sandbox: appConfig.isDev ? true : false,
sandbox: appConfig.isProd ? false : true,
vendorId: appConfig.paddleVendorId,
};

View File

@@ -1,3 +1,4 @@
// TODO: replace with axios-with-proxy when needed
import axios from 'axios';
import appConfig from '../../config/app';
import { DateTime } from 'luxon';

View File

@@ -1,3 +1,4 @@
// TODO: replace with axios-with-proxy
import axios from 'axios';
import appConfig from '../config/app';
import memoryCache from 'memory-cache';

View File

@@ -3,7 +3,7 @@ import ExecutionStep from '../models/execution-step';
import get from 'lodash.get';
// INFO: don't remove space in allowed character group!
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[\da-zA-Z-_ ]+)+}})/g;
const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[\da-zA-Z-_ :]+)+}})/g;
export default function computeParameters(
parameters: Step['parameters'],
@@ -42,7 +42,7 @@ export default function computeParameters(
if (Array.isArray(value)) {
return {
...result,
[key]: value.map(item => computeParameters(item, executionSteps)),
[key]: value.map((item) => computeParameters(item, executionSteps)),
};
}

View File

@@ -3,7 +3,6 @@ import Connection from '../models/connection';
import Flow from '../models/flow';
import Step from '../models/step';
import Execution from '../models/execution';
import appConfig from '../config/app';
import {
IJSONObject,
IApp,
@@ -17,7 +16,7 @@ import AlreadyProcessedError from '../errors/already-processed';
type GlobalVariableOptions = {
connection?: Connection;
app: IApp;
app?: IApp;
flow?: Flow;
step?: Step;
execution?: Execution;
@@ -117,19 +116,22 @@ const globalVariable = async (
$.request = request;
}
if (app) {
$.http = createHttpClient({
$,
baseURL: app.apiBaseUrl,
beforeRequest: app.beforeRequest,
});
if (flow) {
const webhookUrl = appConfig.webhookUrl + '/webhooks/' + flow.id;
$.webhookUrl = webhookUrl;
}
if (isTrigger && (await step.getTriggerCommand()).type === 'webhook') {
if (step) {
$.webhookUrl = await step.getWebhookUrl();
}
if (isTrigger) {
const triggerCommand = await step.getTriggerCommand();
if (triggerCommand.type === 'webhook') {
$.flow.setRemoteWebhookId = async (remoteWebhookId) => {
await flow.$query().patchAndFetch({
remoteWebhookId,
@@ -140,9 +142,10 @@ const globalVariable = async (
$.flow.remoteWebhookId = flow.remoteWebhookId;
}
}
const lastInternalIds =
testRun || (flow && step.isAction) ? [] : await flow?.lastInternalIds(2000);
testRun || (flow && step?.isAction) ? [] : await flow?.lastInternalIds(2000);
const isAlreadyProcessed = (internalId: string) => {
return lastInternalIds?.includes(internalId);

View File

@@ -1,8 +1,10 @@
import axios, { AxiosRequestConfig } from 'axios';
export { AxiosInstance as IHttpClient } from 'axios';
import { IHttpClientParams } from '@automatisch/types';
import { URL } from 'url';
import { AxiosRequestConfig } from 'axios';
import { URL } from 'node:url';
export { AxiosInstance as IHttpClient } from 'axios';
import HttpError from '../../errors/http';
import axios from '../axios-with-proxy';
const removeBaseUrlForAbsoluteUrls = (
requestConfig: AxiosRequestConfig

View File

@@ -2,28 +2,23 @@ import Crypto from 'node:crypto';
import { Response } from 'express';
import { IRequest, ITriggerItem } from '@automatisch/types';
import logger from '../../helpers/logger';
import Flow from '../../models/flow';
import { processTrigger } from '../../services/trigger';
import actionQueue from '../../queues/action';
import globalVariable from '../../helpers/global-variable';
import QuotaExceededError from '../../errors/quote-exceeded';
import Flow from '../models/flow';
import { processTrigger } from '../services/trigger';
import actionQueue from '../queues/action';
import globalVariable from './global-variable';
import QuotaExceededError from '../errors/quote-exceeded';
import {
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
} from '../../helpers/remove-job-configuration';
export default async (request: IRequest, response: Response) => {
const flowId = request.params.flowId;
} from './remove-job-configuration';
export default async (flowId: string, request: IRequest, response: Response) => {
// in case it's our built-in generic webhook trigger
let computedRequestPayload = {
headers: request.headers,
body: request.body,
query: request.query,
};
logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`);
logger.debug(JSON.stringify(computedRequestPayload, null, 2));
const flow = await Flow.query()
.findById(flowId)
@@ -39,32 +34,11 @@ export default async (request: IRequest, response: Response) => {
}
const triggerStep = await flow.getTriggerStep();
const triggerCommand = await triggerStep.getTriggerCommand();
const app = await triggerStep.getApp();
const isWebhookApp = app.key === 'webhook';
if (testRun && !isWebhookApp) {
return response.sendStatus(404);
}
if (triggerCommand.type !== 'webhook') {
return response.sendStatus(404);
}
if (app.auth?.verifyWebhook) {
const $ = await globalVariable({
flow,
connection: await triggerStep.$relatedQuery('connection'),
app,
step: triggerStep,
request,
});
const verified = await app.auth.verifyWebhook($);
if (!verified) {
return response.sendStatus(401);
}
if ((testRun && !isWebhookApp)) {
return response.status(404);
}
// in case trigger type is 'webhook'
@@ -87,7 +61,7 @@ export default async (request: IRequest, response: Response) => {
});
if (testRun) {
return response.sendStatus(204);
return response.status(204);
}
const nextStep = await triggerStep.getNextStep();
@@ -106,5 +80,5 @@ export default async (request: IRequest, response: Response) => {
await actionQueue.add(jobName, jobPayload, jobOptions);
return response.sendStatus(204);
return response.status(204);
};

View File

@@ -1,12 +1,16 @@
import { QueryContext, ModelOptions } from 'objection';
import type { RelationMappings } from 'objection';
import { AES, enc } from 'crypto-js';
import { IRequest } from '@automatisch/types';
import App from './app';
import Base from './base';
import User from './user';
import Step from './step';
import ExtendedQueryBuilder from './query-builder';
import appConfig from '../config/app';
import { IJSONObject } from '@automatisch/types';
import Telemetry from '../helpers/telemetry';
import globalVariable from '../helpers/global-variable';
class Connection extends Base {
id!: string;
@@ -18,6 +22,9 @@ class Connection extends Base {
draft: boolean;
count?: number;
flowCount?: number;
user?: User;
steps?: Step[];
triggerSteps?: Step[];
static tableName = 'connections';
@@ -53,6 +60,17 @@ class Connection extends Base {
to: 'steps.connection_id',
},
},
triggerSteps: {
relation: Base.HasManyRelation,
modelClass: Step,
join: {
from: 'connections.id',
to: 'steps.connection_id',
},
filter(builder: ExtendedQueryBuilder<Step>) {
builder.where('type', '=', 'trigger');
},
},
});
encryptData(): void {
@@ -110,6 +128,27 @@ class Connection extends Base {
await super.$afterUpdate(opt, queryContext);
Telemetry.connectionUpdated(this);
}
async getApp() {
if (!this.key) return null;
return await App.findOneByKey(this.key);
}
async verifyWebhook(request: IRequest) {
if (!this.key) return true;
const app = await this.getApp();
const $ = await globalVariable({
connection: this,
request,
});
if (!app.auth?.verifyWebhook) return true;
return app.auth.verifyWebhook($);
}
}
export default Connection;

View File

@@ -18,9 +18,11 @@ class Flow extends Base {
active: boolean;
status: 'paused' | 'published' | 'draft';
steps: Step[];
triggerStep: Step;
published_at: string;
remoteWebhookId: string;
executions?: Execution[];
lastExecution?: Execution;
user?: User;
static tableName = 'flows';
@@ -50,6 +52,20 @@ class Flow extends Base {
builder.orderBy('position', 'asc');
},
},
triggerStep: {
relation: Base.HasOneRelation,
modelClass: Step,
join: {
from: 'flows.id',
to: 'steps.flow_id',
},
filter(builder: ExtendedQueryBuilder<Step>) {
builder
.where('type', 'trigger')
.limit(1)
.first();
},
},
executions: {
relation: Base.HasManyRelation,
modelClass: Execution,
@@ -58,6 +74,17 @@ class Flow extends Base {
to: 'executions.flow_id',
},
},
lastExecution: {
relation: Base.HasOneRelation,
modelClass: Execution,
join: {
from: 'flows.id',
to: 'executions.flow_id',
},
filter(builder: ExtendedQueryBuilder<Execution>) {
builder.orderBy('created_at', 'desc').limit(1).first();
},
},
user: {
relation: Base.HasOneRelation,
modelClass: User,
@@ -89,10 +116,7 @@ class Flow extends Base {
}
async lastInternalId() {
const lastExecution = await this.$relatedQuery('executions')
.orderBy('created_at', 'desc')
.limit(1)
.first();
const lastExecution = await this.$relatedQuery('lastExecution');
return lastExecution ? (lastExecution as Execution).internalId : null;
}

Some files were not shown because too many files have changed in this diff Show More