diff --git a/packages/backend/src/apps/scheduler/common/cron-times.ts b/packages/backend/src/apps/scheduler/common/cron-times.ts
new file mode 100644
index 00000000..bcc33254
--- /dev/null
+++ b/packages/backend/src/apps/scheduler/common/cron-times.ts
@@ -0,0 +1,10 @@
+const cronTimes = {
+ everyHour: '0 * * * *',
+ everyHourExcludingWeekends: '0 * * * 1-5',
+ everyDayAt: (hour: number) => `0 ${hour} * * *`,
+ everyDayExcludingWeekendsAt: (hour: number) => `0 ${hour} * * 1-5`,
+ everyWeekOnAndAt: (weekday: number, hour: number) => `0 ${hour} * * ${weekday}`,
+ everyMonthOnAndAt: (day: number, hour: number) => `0 ${hour} ${day} * *`,
+};
+
+export default cronTimes;
diff --git a/packages/backend/src/apps/scheduler/common/get-date-time-object.ts b/packages/backend/src/apps/scheduler/common/get-date-time-object.ts
new file mode 100644
index 00000000..9a48cbf2
--- /dev/null
+++ b/packages/backend/src/apps/scheduler/common/get-date-time-object.ts
@@ -0,0 +1,14 @@
+import { DateTime } from 'luxon';
+
+export default function getDateTimeObjectRepresentation(dateTime: DateTime) {
+ const defaults = dateTime.toObject();
+
+ return {
+ ...defaults,
+ ISO_date_time: dateTime.toISO(),
+ pretty_date: dateTime.toLocaleString(DateTime.DATE_MED),
+ pretty_time: dateTime.toLocaleString(DateTime.TIME_WITH_SECONDS),
+ pretty_day_of_week: dateTime.toFormat('cccc'),
+ day_of_week: dateTime.weekday,
+ };
+}
diff --git a/packages/backend/src/apps/scheduler/common/get-next-cron-date-time.ts b/packages/backend/src/apps/scheduler/common/get-next-cron-date-time.ts
new file mode 100644
index 00000000..b339006b
--- /dev/null
+++ b/packages/backend/src/apps/scheduler/common/get-next-cron-date-time.ts
@@ -0,0 +1,10 @@
+import { DateTime } from 'luxon';
+import cronParser from 'cron-parser';
+
+export default function getNextCronDateTime(cronString: string) {
+ const cronDate = cronParser.parseExpression(cronString);
+ const matchingNextCronDateTime = cronDate.next();
+ const matchingNextDateTime = DateTime.fromJSDate(matchingNextCronDateTime.toDate());
+
+ return matchingNextDateTime;
+};
diff --git a/packages/backend/src/apps/scheduler/index.ts b/packages/backend/src/apps/scheduler/index.ts
index b7ee1a76..60f6c4c4 100644
--- a/packages/backend/src/apps/scheduler/index.ts
+++ b/packages/backend/src/apps/scheduler/index.ts
@@ -1,15 +1,10 @@
-import Triggers from './triggers';
-import {
- IService,
- IConnection,
- IFlow,
- IStep,
-} from '@automatisch/types';
-
-export default class Scheduler implements IService {
- triggers: Triggers;
-
- constructor(connection: IConnection, flow: IFlow, step: IStep) {
- this.triggers = new Triggers(step.parameters);
- }
-}
+export default {
+ name: "Scheduler",
+ key: "scheduler",
+ iconUrl: "{BASE_URL}/apps/scheduler/assets/favicon.svg",
+ docUrl: "https://automatisch.io/docs/scheduler",
+ authDocUrl: "https://automatisch.io/docs/connections/scheduler",
+ primaryColor: "0059F7",
+ supportsConnections: false,
+ requiresAuthentication: false,
+};
diff --git a/packages/backend/src/apps/scheduler/info.json b/packages/backend/src/apps/scheduler/info.json
deleted file mode 100644
index d00807d5..00000000
--- a/packages/backend/src/apps/scheduler/info.json
+++ /dev/null
@@ -1,608 +0,0 @@
-{
- "name": "Scheduler",
- "key": "scheduler",
- "iconUrl": "{BASE_URL}/apps/scheduler/assets/favicon.svg",
- "docUrl": "https://automatisch.io/docs/scheduler",
- "authDocUrl": "https://automatisch.io/docs/connections/scheduler",
- "primaryColor": "0059F7",
- "supportsConnections": false,
- "requiresAuthentication": false,
- "triggers": [
- {
- "name": "Every hour",
- "key": "everyHour",
- "description": "Triggers every hour.",
- "substeps": [
- {
- "key": "chooseTrigger",
- "name": "Set up a trigger",
- "arguments": [
- {
- "label": "Trigger on weekends?",
- "key": "triggersOnWeekend",
- "type": "dropdown",
- "description": "Should this flow trigger on Saturday and Sunday?",
- "required": true,
- "value": true,
- "variables": false,
- "options": [
- {
- "label": "Yes",
- "value": true
- },
- {
- "label": "No",
- "value": false
- }
- ]
- }
- ]
- },
- {
- "key": "testStep",
- "name": "Test trigger"
- }
- ]
- },
- {
- "name": "Every day",
- "key": "everyDay",
- "description": "Triggers every day.",
- "substeps": [
- {
- "key": "chooseTrigger",
- "name": "Set up a trigger",
- "arguments": [
- {
- "label": "Trigger on weekends?",
- "key": "triggersOnWeekend",
- "type": "dropdown",
- "description": "Should this flow trigger on Saturday and Sunday?",
- "required": true,
- "value": true,
- "variables": false,
- "options": [
- {
- "label": "Yes",
- "value": true
- },
- {
- "label": "No",
- "value": false
- }
- ]
- },
- {
- "label": "Time of day",
- "key": "hour",
- "type": "dropdown",
- "required": true,
- "value": null,
- "variables": false,
- "options": [
- {
- "label": "00:00",
- "value": 0
- },
- {
- "label": "01:00",
- "value": 1
- },
- {
- "label": "02:00",
- "value": 2
- },
- {
- "label": "03:00",
- "value": 3
- },
- {
- "label": "04:00",
- "value": 4
- },
- {
- "label": "05:00",
- "value": 5
- },
- {
- "label": "06:00",
- "value": 6
- },
- {
- "label": "07:00",
- "value": 7
- },
- {
- "label": "08:00",
- "value": 8
- },
- {
- "label": "09:00",
- "value": 9
- },
- {
- "label": "10:00",
- "value": 10
- },
- {
- "label": "11:00",
- "value": 11
- },
- {
- "label": "12:00",
- "value": 12
- },
- {
- "label": "13:00",
- "value": 13
- },
- {
- "label": "14:00",
- "value": 14
- },
- {
- "label": "15:00",
- "value": 15
- },
- {
- "label": "16:00",
- "value": 16
- },
- {
- "label": "17:00",
- "value": 17
- },
- {
- "label": "18:00",
- "value": 18
- },
- {
- "label": "19:00",
- "value": 19
- },
- {
- "label": "20:00",
- "value": 20
- },
- {
- "label": "21:00",
- "value": 21
- },
- {
- "label": "22:00",
- "value": 22
- },
- {
- "label": "23:00",
- "value": 23
- }
- ]
- }
- ]
- },
- {
- "key": "testStep",
- "name": "Test trigger"
- }
- ]
- },
- {
- "name": "Every week",
- "key": "everyWeek",
- "description": "Triggers every week.",
- "substeps": [
- {
- "key": "chooseTrigger",
- "name": "Set up a trigger",
- "arguments": [
- {
- "label": "Day of the week",
- "key": "weekday",
- "type": "dropdown",
- "required": true,
- "value": null,
- "variables": false,
- "options": [
- {
- "label": "Monday",
- "value": 1
- },
- {
- "label": "Tuesday",
- "value": 2
- },
- {
- "label": "Wednesday",
- "value": 3
- },
- {
- "label": "Thursday",
- "value": 4
- },
- {
- "label": "Friday",
- "value": 5
- },
- {
- "label": "Saturday",
- "value": 6
- },
- {
- "label": "Sunday",
- "value": 0
- }
- ]
- },
- {
- "label": "Time of day",
- "key": "hour",
- "type": "dropdown",
- "required": true,
- "value": null,
- "variables": false,
- "options": [
- {
- "label": "00:00",
- "value": 0
- },
- {
- "label": "01:00",
- "value": 1
- },
- {
- "label": "02:00",
- "value": 2
- },
- {
- "label": "03:00",
- "value": 3
- },
- {
- "label": "04:00",
- "value": 4
- },
- {
- "label": "05:00",
- "value": 5
- },
- {
- "label": "06:00",
- "value": 6
- },
- {
- "label": "07:00",
- "value": 7
- },
- {
- "label": "08:00",
- "value": 8
- },
- {
- "label": "09:00",
- "value": 9
- },
- {
- "label": "10:00",
- "value": 10
- },
- {
- "label": "11:00",
- "value": 11
- },
- {
- "label": "12:00",
- "value": 12
- },
- {
- "label": "13:00",
- "value": 13
- },
- {
- "label": "14:00",
- "value": 14
- },
- {
- "label": "15:00",
- "value": 15
- },
- {
- "label": "16:00",
- "value": 16
- },
- {
- "label": "17:00",
- "value": 17
- },
- {
- "label": "18:00",
- "value": 18
- },
- {
- "label": "19:00",
- "value": 19
- },
- {
- "label": "20:00",
- "value": 20
- },
- {
- "label": "21:00",
- "value": 21
- },
- {
- "label": "22:00",
- "value": 22
- },
- {
- "label": "23:00",
- "value": 23
- }
- ]
- }
- ]
- },
- {
- "key": "testStep",
- "name": "Test trigger"
- }
- ]
- },
- {
- "name": "Every month",
- "key": "everyMonth",
- "description": "Triggers every month.",
- "substeps": [
- {
- "key": "chooseTrigger",
- "name": "Set up a trigger",
- "arguments": [
- {
- "label": "Day of the month",
- "key": "day",
- "type": "dropdown",
- "required": true,
- "value": null,
- "variables": false,
- "options": [
- {
- "label": 1,
- "value": 1
- },
- {
- "label": 2,
- "value": 2
- },
- {
- "label": 3,
- "value": 3
- },
- {
- "label": 4,
- "value": 4
- },
- {
- "label": 5,
- "value": 5
- },
- {
- "label": 6,
- "value": 6
- },
- {
- "label": 7,
- "value": 7
- },
- {
- "label": 8,
- "value": 8
- },
- {
- "label": 9,
- "value": 9
- },
- {
- "label": 10,
- "value": 10
- },
- {
- "label": 11,
- "value": 11
- },
- {
- "label": 12,
- "value": 12
- },
- {
- "label": 13,
- "value": 13
- },
- {
- "label": 14,
- "value": 14
- },
- {
- "label": 15,
- "value": 15
- },
- {
- "label": 16,
- "value": 16
- },
- {
- "label": 17,
- "value": 17
- },
- {
- "label": 18,
- "value": 18
- },
- {
- "label": 19,
- "value": 19
- },
- {
- "label": 20,
- "value": 20
- },
- {
- "label": 21,
- "value": 21
- },
- {
- "label": 22,
- "value": 22
- },
- {
- "label": 23,
- "value": 23
- },
- {
- "label": 24,
- "value": 24
- },
- {
- "label": 25,
- "value": 25
- },
- {
- "label": 26,
- "value": 26
- },
- {
- "label": 27,
- "value": 27
- },
- {
- "label": 28,
- "value": 28
- },
- {
- "label": 29,
- "value": 29
- },
- {
- "label": 30,
- "value": 30
- },
- {
- "label": 31,
- "value": 31
- }
- ]
- },
- {
- "label": "Time of day",
- "key": "hour",
- "type": "dropdown",
- "required": true,
- "value": null,
- "variables": false,
- "options": [
- {
- "label": "00:00",
- "value": 0
- },
- {
- "label": "01:00",
- "value": 1
- },
- {
- "label": "02:00",
- "value": 2
- },
- {
- "label": "03:00",
- "value": 3
- },
- {
- "label": "04:00",
- "value": 4
- },
- {
- "label": "05:00",
- "value": 5
- },
- {
- "label": "06:00",
- "value": 6
- },
- {
- "label": "07:00",
- "value": 7
- },
- {
- "label": "08:00",
- "value": 8
- },
- {
- "label": "09:00",
- "value": 9
- },
- {
- "label": "10:00",
- "value": 10
- },
- {
- "label": "11:00",
- "value": 11
- },
- {
- "label": "12:00",
- "value": 12
- },
- {
- "label": "13:00",
- "value": 13
- },
- {
- "label": "14:00",
- "value": 14
- },
- {
- "label": "15:00",
- "value": 15
- },
- {
- "label": "16:00",
- "value": 16
- },
- {
- "label": "17:00",
- "value": 17
- },
- {
- "label": "18:00",
- "value": 18
- },
- {
- "label": "19:00",
- "value": 19
- },
- {
- "label": "20:00",
- "value": 20
- },
- {
- "label": "21:00",
- "value": 21
- },
- {
- "label": "22:00",
- "value": 22
- },
- {
- "label": "23:00",
- "value": 23
- }
- ]
- }
- ]
- },
- {
- "key": "testStep",
- "name": "Test trigger"
- }
- ]
- }
- ]
-}
diff --git a/packages/backend/src/apps/scheduler/triggers.ts b/packages/backend/src/apps/scheduler/triggers.ts
deleted file mode 100644
index ef959681..00000000
--- a/packages/backend/src/apps/scheduler/triggers.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { IStep } from '@automatisch/types';
-import EveryHour from './triggers/every-hour';
-import EveryDay from './triggers/every-day';
-import EveryWeek from './triggers/every-week';
-import EveryMonth from './triggers/every-month';
-
-export default class Triggers {
- everyHour: EveryHour;
- everyDay: EveryDay;
- everyWeek: EveryWeek;
- everyMonth: EveryMonth;
-
- constructor(parameters: IStep["parameters"]) {
- this.everyHour = new EveryHour(parameters);
- this.everyDay = new EveryDay(parameters);
- this.everyWeek = new EveryWeek(parameters);
- this.everyMonth = new EveryMonth(parameters);
- }
-}
diff --git a/packages/backend/src/apps/scheduler/triggers/every-day.ts b/packages/backend/src/apps/scheduler/triggers/every-day.ts
deleted file mode 100644
index dceb2d7e..00000000
--- a/packages/backend/src/apps/scheduler/triggers/every-day.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import { DateTime } from 'luxon';
-import type { IStep, IJSONValue, ITrigger } from '@automatisch/types';
-import { cronTimes, getNextCronDateTime, getDateTimeObjectRepresentation } from '../utils';
-
-export default class EveryDay implements ITrigger {
- triggersOnWeekend?: boolean;
- hour?: number;
-
- constructor(parameters: IStep["parameters"]) {
- if (parameters.triggersOnWeekend) {
- this.triggersOnWeekend = parameters.triggersOnWeekend as boolean;
- }
-
- if (parameters.hour) {
- this.hour = parameters.hour as number;
- }
- }
-
- get interval() {
- if (this.triggersOnWeekend) {
- return cronTimes.everyDayAt(this.hour);
- }
-
- return cronTimes.everyDayExcludingWeekendsAt(this.hour);
- }
-
- async run(startDateTime: Date) {
- const dateTime = DateTime.fromJSDate(startDateTime);
- const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
-
- return [dateTimeObjectRepresentation] as IJSONValue;
- }
-
- async testRun() {
- const nextCronDateTime = getNextCronDateTime(this.interval);
- const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
-
- return [dateTimeObjectRepresentation] as IJSONValue;
- }
-}
diff --git a/packages/backend/src/apps/scheduler/triggers/every-day/index.ts b/packages/backend/src/apps/scheduler/triggers/every-day/index.ts
new file mode 100644
index 00000000..faed677a
--- /dev/null
+++ b/packages/backend/src/apps/scheduler/triggers/every-day/index.ts
@@ -0,0 +1,170 @@
+import { DateTime } from 'luxon';
+import { IGlobalVariable, IJSONValue } from '@automatisch/types';
+import cronTimes from '../../common/cron-times';
+import getNextCronDateTime from '../../common/get-next-cron-date-time';
+import getDateTimeObjectRepresentation from '../../common/get-date-time-object';
+
+export default {
+ name: 'Every day',
+ key: 'everyDay',
+ description: 'Triggers every day.',
+ substeps: [
+ {
+ key: 'chooseTrigger',
+ name: 'Set up a trigger',
+ arguments: [
+ {
+ label: 'Trigger on weekends?',
+ key: 'triggersOnWeekend',
+ type: 'dropdown',
+ description: 'Should this flow trigger on Saturday and Sunday?',
+ required: true,
+ value: true,
+ variables: false,
+ options: [
+ {
+ label: 'Yes',
+ value: true
+ },
+ {
+ label: 'No',
+ value: false
+ }
+ ]
+ },
+ {
+ label: 'Time of day',
+ key: 'hour',
+ type: 'dropdown',
+ required: true,
+ value: null,
+ variables: false,
+ options: [
+ {
+ label: '00:00',
+ value: 0
+ },
+ {
+ label: '01:00',
+ value: 1
+ },
+ {
+ label: '02:00',
+ value: 2
+ },
+ {
+ label: '03:00',
+ value: 3
+ },
+ {
+ label: '04:00',
+ value: 4
+ },
+ {
+ label: '05:00',
+ value: 5
+ },
+ {
+ label: '06:00',
+ value: 6
+ },
+ {
+ label: '07:00',
+ value: 7
+ },
+ {
+ label: '08:00',
+ value: 8
+ },
+ {
+ label: '09:00',
+ value: 9
+ },
+ {
+ label: '10:00',
+ value: 10
+ },
+ {
+ label: '11:00',
+ value: 11
+ },
+ {
+ label: '12:00',
+ value: 12
+ },
+ {
+ label: '13:00',
+ value: 13
+ },
+ {
+ label: '14:00',
+ value: 14
+ },
+ {
+ label: '15:00',
+ value: 15
+ },
+ {
+ label: '16:00',
+ value: 16
+ },
+ {
+ label: '17:00',
+ value: 17
+ },
+ {
+ label: '18:00',
+ value: 18
+ },
+ {
+ label: '19:00',
+ value: 19
+ },
+ {
+ label: '20:00',
+ value: 20
+ },
+ {
+ label: '21:00',
+ value: 21
+ },
+ {
+ label: '22:00',
+ value: 22
+ },
+ {
+ label: '23:00',
+ value: 23
+ }
+ ]
+ }
+ ]
+ },
+ {
+ key: 'testStep',
+ name: 'Test trigger'
+ }
+ ],
+
+ getInterval(parameters: IGlobalVariable["db"]["step"]["parameters"]) {
+ if (parameters.triggersOnWeekend as boolean) {
+ return cronTimes.everyDayAt(parameters.hour as number);
+ }
+
+ return cronTimes.everyDayExcludingWeekendsAt(parameters.hour as number);
+ },
+
+ async run($: IGlobalVariable, startDateTime: Date) {
+ const dateTime = DateTime.fromJSDate(startDateTime);
+ const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
+
+ return [dateTimeObjectRepresentation] as IJSONValue;
+ },
+
+ async testRun($: IGlobalVariable) {
+ const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
+ const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
+
+ return [dateTimeObjectRepresentation] as IJSONValue;
+ },
+};
diff --git a/packages/backend/src/apps/scheduler/triggers/every-hour.ts b/packages/backend/src/apps/scheduler/triggers/every-hour.ts
deleted file mode 100644
index f76c9cf6..00000000
--- a/packages/backend/src/apps/scheduler/triggers/every-hour.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { DateTime } from 'luxon';
-import type { IStep, IJSONValue, ITrigger } from '@automatisch/types';
-import { cronTimes, getNextCronDateTime, getDateTimeObjectRepresentation } from '../utils';
-
-export default class EveryHour implements ITrigger {
- triggersOnWeekend?: boolean | string;
-
- constructor(parameters: IStep["parameters"]) {
- if (parameters.triggersOnWeekend) {
- this.triggersOnWeekend = parameters.triggersOnWeekend as string;
- }
- }
-
- get interval() {
- if (this.triggersOnWeekend) {
- return cronTimes.everyHour;
- }
-
- return cronTimes.everyHourExcludingWeekends;
- }
-
- async run(startDateTime: Date) {
- const dateTime = DateTime.fromJSDate(startDateTime);
- const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
-
- return [dateTimeObjectRepresentation] as IJSONValue;
- }
-
- async testRun() {
- const nextCronDateTime = getNextCronDateTime(this.interval);
- const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
-
- return [dateTimeObjectRepresentation] as IJSONValue;
- }
-}
diff --git a/packages/backend/src/apps/scheduler/triggers/every-hour/index.ts b/packages/backend/src/apps/scheduler/triggers/every-hour/index.ts
new file mode 100644
index 00000000..e13e7d3d
--- /dev/null
+++ b/packages/backend/src/apps/scheduler/triggers/every-hour/index.ts
@@ -0,0 +1,64 @@
+import { DateTime } from 'luxon';
+import { IGlobalVariable, IJSONValue } from '@automatisch/types';
+import cronTimes from '../../common/cron-times';
+import getNextCronDateTime from '../../common/get-next-cron-date-time';
+import getDateTimeObjectRepresentation from '../../common/get-date-time-object';
+
+export default {
+ name: 'Every hour',
+ key: 'everyHour',
+ description: 'Triggers every hour.',
+ substeps: [
+ {
+ key: 'chooseTrigger',
+ name: 'Set up a trigger',
+ arguments: [
+ {
+ label: 'Trigger on weekends?',
+ key: 'triggersOnWeekend',
+ type: 'dropdown',
+ description: 'Should this flow trigger on Saturday and Sunday?',
+ required: true,
+ value: true,
+ variables: false,
+ options: [
+ {
+ label: 'Yes',
+ value: true
+ },
+ {
+ label: 'No',
+ value: false
+ }
+ ]
+ }
+ ]
+ },
+ {
+ key: 'testStep',
+ name: 'Test trigger'
+ }
+ ],
+
+ getInterval(parameters: IGlobalVariable["db"]["step"]["parameters"]) {
+ if (parameters.triggersOnWeekend) {
+ return cronTimes.everyHour
+ }
+
+ return cronTimes.everyHourExcludingWeekends;
+ },
+
+ async run($: IGlobalVariable, startDateTime: Date) {
+ const dateTime = DateTime.fromJSDate(startDateTime);
+ const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
+
+ return [dateTimeObjectRepresentation] as IJSONValue;
+ },
+
+ async testRun($: IGlobalVariable) {
+ const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
+ const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
+
+ return [dateTimeObjectRepresentation] as IJSONValue;
+ },
+};
diff --git a/packages/backend/src/apps/scheduler/triggers/every-month.ts b/packages/backend/src/apps/scheduler/triggers/every-month.ts
deleted file mode 100644
index 822e3b31..00000000
--- a/packages/backend/src/apps/scheduler/triggers/every-month.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { DateTime } from 'luxon';
-import type { IStep, IJSONValue, ITrigger } from '@automatisch/types';
-import { cronTimes, getNextCronDateTime, getDateTimeObjectRepresentation } from '../utils';
-
-export default class EveryMonth implements ITrigger {
- day?: number;
- hour?: number;
-
- constructor(parameters: IStep["parameters"]) {
- if (parameters.day) {
- this.day = parameters.day as number;
- }
-
- if (parameters.hour) {
- this.hour = parameters.hour as number;
- }
- }
-
- get interval() {
- return cronTimes.everyMonthOnAndAt(this.day, this.hour);
- }
-
- async run(startDateTime: Date) {
- const dateTime = DateTime.fromJSDate(startDateTime);
- const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
-
- return [dateTimeObjectRepresentation] as IJSONValue;
- }
-
- async testRun() {
- const nextCronDateTime = getNextCronDateTime(this.interval);
- const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
-
- return [dateTimeObjectRepresentation] as IJSONValue;
- }
-}
diff --git a/packages/backend/src/apps/scheduler/triggers/every-month/index.ts b/packages/backend/src/apps/scheduler/triggers/every-month/index.ts
new file mode 100644
index 00000000..203a1393
--- /dev/null
+++ b/packages/backend/src/apps/scheduler/triggers/every-month/index.ts
@@ -0,0 +1,283 @@
+import { DateTime } from 'luxon';
+import { IGlobalVariable, IJSONValue } from '@automatisch/types';
+import cronTimes from '../../common/cron-times';
+import getNextCronDateTime from '../../common/get-next-cron-date-time';
+import getDateTimeObjectRepresentation from '../../common/get-date-time-object';
+
+export default {
+ name: 'Every month',
+ key: 'everyMonth',
+ description: 'Triggers every month.',
+ substeps: [
+ {
+ key: 'chooseTrigger',
+ name: 'Set up a trigger',
+ arguments: [
+ {
+ label: 'Day of the month',
+ key: 'day',
+ type: 'dropdown',
+ required: true,
+ value: null,
+ variables: false,
+ options: [
+ {
+ label: 1,
+ value: 1
+ },
+ {
+ label: 2,
+ value: 2
+ },
+ {
+ label: 3,
+ value: 3
+ },
+ {
+ label: 4,
+ value: 4
+ },
+ {
+ label: 5,
+ value: 5
+ },
+ {
+ label: 6,
+ value: 6
+ },
+ {
+ label: 7,
+ value: 7
+ },
+ {
+ label: 8,
+ value: 8
+ },
+ {
+ label: 9,
+ value: 9
+ },
+ {
+ label: 10,
+ value: 10
+ },
+ {
+ label: 11,
+ value: 11
+ },
+ {
+ label: 12,
+ value: 12
+ },
+ {
+ label: 13,
+ value: 13
+ },
+ {
+ label: 14,
+ value: 14
+ },
+ {
+ label: 15,
+ value: 15
+ },
+ {
+ label: 16,
+ value: 16
+ },
+ {
+ label: 17,
+ value: 17
+ },
+ {
+ label: 18,
+ value: 18
+ },
+ {
+ label: 19,
+ value: 19
+ },
+ {
+ label: 20,
+ value: 20
+ },
+ {
+ label: 21,
+ value: 21
+ },
+ {
+ label: 22,
+ value: 22
+ },
+ {
+ label: 23,
+ value: 23
+ },
+ {
+ label: 24,
+ value: 24
+ },
+ {
+ label: 25,
+ value: 25
+ },
+ {
+ label: 26,
+ value: 26
+ },
+ {
+ label: 27,
+ value: 27
+ },
+ {
+ label: 28,
+ value: 28
+ },
+ {
+ label: 29,
+ value: 29
+ },
+ {
+ label: 30,
+ value: 30
+ },
+ {
+ label: 31,
+ value: 31
+ }
+ ]
+ },
+ {
+ label: 'Time of day',
+ key: 'hour',
+ type: 'dropdown',
+ required: true,
+ value: null,
+ variables: false,
+ options: [
+ {
+ label: '00:00',
+ value: 0
+ },
+ {
+ label: '01:00',
+ value: 1
+ },
+ {
+ label: '02:00',
+ value: 2
+ },
+ {
+ label: '03:00',
+ value: 3
+ },
+ {
+ label: '04:00',
+ value: 4
+ },
+ {
+ label: '05:00',
+ value: 5
+ },
+ {
+ label: '06:00',
+ value: 6
+ },
+ {
+ label: '07:00',
+ value: 7
+ },
+ {
+ label: '08:00',
+ value: 8
+ },
+ {
+ label: '09:00',
+ value: 9
+ },
+ {
+ label: '10:00',
+ value: 10
+ },
+ {
+ label: '11:00',
+ value: 11
+ },
+ {
+ label: '12:00',
+ value: 12
+ },
+ {
+ label: '13:00',
+ value: 13
+ },
+ {
+ label: '14:00',
+ value: 14
+ },
+ {
+ label: '15:00',
+ value: 15
+ },
+ {
+ label: '16:00',
+ value: 16
+ },
+ {
+ label: '17:00',
+ value: 17
+ },
+ {
+ label: '18:00',
+ value: 18
+ },
+ {
+ label: '19:00',
+ value: 19
+ },
+ {
+ label: '20:00',
+ value: 20
+ },
+ {
+ label: '21:00',
+ value: 21
+ },
+ {
+ label: '22:00',
+ value: 22
+ },
+ {
+ label: '23:00',
+ value: 23
+ }
+ ]
+ }
+ ]
+ },
+ {
+ key: 'testStep',
+ name: 'Test trigger'
+ }
+ ],
+
+ getInterval(parameters: IGlobalVariable["db"]["step"]["parameters"]) {
+ const interval = cronTimes.everyMonthOnAndAt(parameters.day as number, parameters.hour as number);
+
+ return interval;
+ },
+
+ async run($: IGlobalVariable, startDateTime: Date) {
+ const dateTime = DateTime.fromJSDate(startDateTime);
+ const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
+
+ return [dateTimeObjectRepresentation] as IJSONValue;
+ },
+
+ async testRun($: IGlobalVariable) {
+ const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
+ const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
+
+ return [dateTimeObjectRepresentation] as IJSONValue;
+ },
+};
diff --git a/packages/backend/src/apps/scheduler/triggers/every-week.ts b/packages/backend/src/apps/scheduler/triggers/every-week.ts
deleted file mode 100644
index 06012f7e..00000000
--- a/packages/backend/src/apps/scheduler/triggers/every-week.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { DateTime } from 'luxon';
-import type { IStep, IJSONValue, ITrigger } from '@automatisch/types';
-import { cronTimes, getNextCronDateTime, getDateTimeObjectRepresentation } from '../utils';
-
-export default class EveryWeek implements ITrigger {
- weekday?: number;
- hour?: number;
-
- constructor(parameters: IStep["parameters"]) {
- if (parameters.weekday) {
- this.weekday = parameters.weekday as number;
- }
-
- if (parameters.hour) {
- this.hour = parameters.hour as number;
- }
- }
-
- get interval() {
- return cronTimes.everyWeekOnAndAt(this.weekday, this.hour);
- }
-
- async run(startDateTime: Date) {
- const dateTime = DateTime.fromJSDate(startDateTime);
- const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
-
- return [dateTimeObjectRepresentation] as IJSONValue;
- }
-
- async testRun() {
- const nextCronDateTime = getNextCronDateTime(this.interval);
- const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
-
- return [dateTimeObjectRepresentation] as IJSONValue;
- }
-}
diff --git a/packages/backend/src/apps/scheduler/triggers/every-week/index.ts b/packages/backend/src/apps/scheduler/triggers/every-week/index.ts
new file mode 100644
index 00000000..72479cb3
--- /dev/null
+++ b/packages/backend/src/apps/scheduler/triggers/every-week/index.ts
@@ -0,0 +1,187 @@
+import { DateTime } from 'luxon';
+import { IGlobalVariable, IJSONValue } from '@automatisch/types';
+import cronTimes from '../../common/cron-times';
+import getNextCronDateTime from '../../common/get-next-cron-date-time';
+import getDateTimeObjectRepresentation from '../../common/get-date-time-object';
+
+export default {
+ name: 'Every week',
+ key: 'everyWeek',
+ description: 'Triggers every week.',
+ substeps: [
+ {
+ key: 'chooseTrigger',
+ name: 'Set up a trigger',
+ arguments: [
+ {
+ label: 'Day of the week',
+ key: 'weekday',
+ type: 'dropdown',
+ required: true,
+ value: null,
+ variables: false,
+ options: [
+ {
+ label: 'Monday',
+ value: 1
+ },
+ {
+ label: 'Tuesday',
+ value: 2
+ },
+ {
+ label: 'Wednesday',
+ value: 3
+ },
+ {
+ label: 'Thursday',
+ value: 4
+ },
+ {
+ label: 'Friday',
+ value: 5
+ },
+ {
+ label: 'Saturday',
+ value: 6
+ },
+ {
+ label: 'Sunday',
+ value: 0
+ }
+ ]
+ },
+ {
+ label: 'Time of day',
+ key: 'hour',
+ type: 'dropdown',
+ required: true,
+ value: null,
+ variables: false,
+ options: [
+ {
+ label: '00:00',
+ value: 0
+ },
+ {
+ label: '01:00',
+ value: 1
+ },
+ {
+ label: '02:00',
+ value: 2
+ },
+ {
+ label: '03:00',
+ value: 3
+ },
+ {
+ label: '04:00',
+ value: 4
+ },
+ {
+ label: '05:00',
+ value: 5
+ },
+ {
+ label: '06:00',
+ value: 6
+ },
+ {
+ label: '07:00',
+ value: 7
+ },
+ {
+ label: '08:00',
+ value: 8
+ },
+ {
+ label: '09:00',
+ value: 9
+ },
+ {
+ label: '10:00',
+ value: 10
+ },
+ {
+ label: '11:00',
+ value: 11
+ },
+ {
+ label: '12:00',
+ value: 12
+ },
+ {
+ label: '13:00',
+ value: 13
+ },
+ {
+ label: '14:00',
+ value: 14
+ },
+ {
+ label: '15:00',
+ value: 15
+ },
+ {
+ label: '16:00',
+ value: 16
+ },
+ {
+ label: '17:00',
+ value: 17
+ },
+ {
+ label: '18:00',
+ value: 18
+ },
+ {
+ label: '19:00',
+ value: 19
+ },
+ {
+ label: '20:00',
+ value: 20
+ },
+ {
+ label: '21:00',
+ value: 21
+ },
+ {
+ label: '22:00',
+ value: 22
+ },
+ {
+ label: '23:00',
+ value: 23
+ }
+ ]
+ }
+ ]
+ },
+ {
+ key: 'testStep',
+ name: 'Test trigger'
+ }
+ ],
+
+ getInterval(parameters: IGlobalVariable["db"]["step"]["parameters"]) {
+ const interval = cronTimes.everyWeekOnAndAt(parameters.weekday as number, parameters.hour as number);
+
+ return interval;
+ },
+
+ async run($: IGlobalVariable, startDateTime: Date) {
+ const dateTime = DateTime.fromJSDate(startDateTime);
+ const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(dateTime) as IJSONValue;
+
+ return [dateTimeObjectRepresentation] as IJSONValue;
+ },
+
+ async testRun($: IGlobalVariable) {
+ const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
+ const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
+
+ return [dateTimeObjectRepresentation] as IJSONValue;
+ },
+};
diff --git a/packages/backend/src/apps/scheduler/utils.ts b/packages/backend/src/apps/scheduler/utils.ts
deleted file mode 100644
index 192ba102..00000000
--- a/packages/backend/src/apps/scheduler/utils.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { DateTime } from 'luxon';
-import cronParser from 'cron-parser';
-
-export const cronTimes = {
- everyHour: '0 * * * *',
- everyHourExcludingWeekends: '0 * * * 1-5',
- everyDayAt: (hour: number) => `0 ${hour} * * *`,
- everyDayExcludingWeekendsAt: (hour: number) => `0 ${hour} * * 1-5`,
- everyWeekOnAndAt: (weekday: number, hour: number) => `0 ${hour} * * ${weekday}`,
- everyMonthOnAndAt: (day: number, hour: number) => `0 ${hour} ${day} * *`,
-};
-
-export function getNextCronDateTime(cronString: string) {
- const cronDate = cronParser.parseExpression(cronString);
- const matchingNextCronDateTime = cronDate.next();
- const matchingNextDateTime = DateTime.fromJSDate(matchingNextCronDateTime.toDate());
-
- return matchingNextDateTime;
-};
-
-export function getDateTimeObjectRepresentation(dateTime: DateTime) {
- const defaults = dateTime.toObject();
-
- return {
- ...defaults,
- ISO_date_time: dateTime.toISO(),
- pretty_date: dateTime.toLocaleString(DateTime.DATE_MED),
- pretty_time: dateTime.toLocaleString(DateTime.TIME_WITH_SECONDS),
- pretty_day_of_week: dateTime.toFormat('cccc'),
- day_of_week: dateTime.weekday,
- };
-}
diff --git a/packages/backend/src/apps/slack/actions.ts b/packages/backend/src/apps/slack/actions.ts
deleted file mode 100644
index e4063b76..00000000
--- a/packages/backend/src/apps/slack/actions.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import SendMessageToChannel from './actions/send-message-to-channel';
-import FindMessage from './actions/find-message';
-import SlackClient from './client';
-
-export default class Actions {
- client: SlackClient;
- sendMessageToChannel: SendMessageToChannel;
- findMessage: FindMessage;
-
- constructor(client: SlackClient) {
- this.client = client;
- this.sendMessageToChannel = new SendMessageToChannel(client);
- this.findMessage = new FindMessage(client);
- }
-}
diff --git a/packages/backend/src/apps/slack/actions/find-message.ts b/packages/backend/src/apps/slack/actions/find-message.ts
deleted file mode 100644
index 4185d7b5..00000000
--- a/packages/backend/src/apps/slack/actions/find-message.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import SlackClient from '../client';
-
-export default class FindMessage {
- client: SlackClient;
-
- constructor(client: SlackClient) {
- this.client = client;
- }
-
- async run() {
- const parameters = this.client.step.parameters;
- const query = parameters.query as string;
- const sortBy = parameters.sortBy as string;
- const sortDirection = parameters.sortDirection as string;
- const count = 1;
-
- const messages = await this.client.findMessages.run(
- query,
- sortBy,
- sortDirection,
- count,
- );
-
- return messages;
- }
-}
diff --git a/packages/backend/src/apps/slack/actions/find-message/find-message.ts b/packages/backend/src/apps/slack/actions/find-message/find-message.ts
new file mode 100644
index 00000000..57c93041
--- /dev/null
+++ b/packages/backend/src/apps/slack/actions/find-message/find-message.ts
@@ -0,0 +1,50 @@
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+
+type FindMessageOptions = {
+ query: string;
+ sortBy: string;
+ sortDirection: string;
+ count: number;
+};
+
+const findMessage = async ($: IGlobalVariable, options: FindMessageOptions) => {
+ const message: {
+ data?: IJSONObject;
+ error?: IJSONObject;
+ } = {};
+
+ const headers = {
+ Authorization: `Bearer ${$.auth.data.accessToken}`,
+ };
+
+ const params = {
+ query: options.query,
+ sort: options.sortBy,
+ sort_dir: options.sortDirection,
+ count: options.count || 1,
+ };
+
+ const response = await $.http.get('/search.messages', {
+ headers,
+ params,
+ });
+
+ if (response.integrationError) {
+ message.error = response.integrationError;
+ return message;
+ }
+
+ const data = response.data;
+
+ if (!data.ok) {
+ message.error = data;
+ return message;
+ }
+
+ const messages = data.messages.matches;
+ message.data = messages?.[0];
+
+ return message;
+};
+
+export default findMessage;
diff --git a/packages/backend/src/apps/slack/actions/find-message/index.ts b/packages/backend/src/apps/slack/actions/find-message/index.ts
new file mode 100644
index 00000000..c6132e2c
--- /dev/null
+++ b/packages/backend/src/apps/slack/actions/find-message/index.ts
@@ -0,0 +1,90 @@
+import { IGlobalVariable } from '@automatisch/types';
+import findMessage from './find-message';
+
+export default {
+ name: 'Find message',
+ key: 'findMessage',
+ description: 'Find a Slack message using the Slack Search feature.',
+ substeps: [
+ {
+ key: 'chooseConnection',
+ name: 'Choose connection',
+ },
+ {
+ key: 'setupAction',
+ name: 'Set up action',
+ arguments: [
+ {
+ label: 'Search Query',
+ key: 'query',
+ type: 'string',
+ required: true,
+ description:
+ 'Search query to use for finding matching messages. See the Slack Search Documentation for more information on constructing a query.',
+ variables: true,
+ },
+ {
+ label: 'Sort by',
+ key: 'sortBy',
+ type: 'dropdown',
+ description:
+ 'Sort messages by their match strength or by their date. Default is score.',
+ required: true,
+ value: 'score',
+ variables: false,
+ options: [
+ {
+ label: 'Match strength',
+ value: 'score',
+ },
+ {
+ label: 'Message date time',
+ value: 'timestamp',
+ },
+ ],
+ },
+ {
+ label: 'Sort direction',
+ key: 'sortDirection',
+ type: 'dropdown',
+ description:
+ 'Sort matching messages in ascending or descending order. Default is descending.',
+ required: true,
+ value: 'desc',
+ variables: false,
+ options: [
+ {
+ label: 'Descending (newest or best match first)',
+ value: 'desc',
+ },
+ {
+ label: 'Ascending (oldest or worst match first)',
+ value: 'asc',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ key: 'testStep',
+ name: 'Test action',
+ },
+ ],
+
+ async run($: IGlobalVariable) {
+ const parameters = $.db.step.parameters;
+ const query = parameters.query as string;
+ const sortBy = parameters.sortBy as string;
+ const sortDirection = parameters.sortDirection as string;
+ const count = 1;
+
+ const messages = await findMessage($, {
+ query,
+ sortBy,
+ sortDirection,
+ count,
+ });
+
+ return messages;
+ },
+};
diff --git a/packages/backend/src/apps/slack/actions/send-a-message-to-channel/index.ts b/packages/backend/src/apps/slack/actions/send-a-message-to-channel/index.ts
new file mode 100644
index 00000000..d2033c1e
--- /dev/null
+++ b/packages/backend/src/apps/slack/actions/send-a-message-to-channel/index.ts
@@ -0,0 +1,59 @@
+import { IGlobalVariable } from '@automatisch/types';
+import postMessage from './post-message';
+
+export default {
+ name: 'Send a message to channel',
+ key: 'sendMessageToChannel',
+ description: 'Send a message to a specific channel you specify.',
+ substeps: [
+ {
+ key: 'chooseConnection',
+ name: 'Choose connection',
+ },
+ {
+ key: 'setupAction',
+ name: 'Set up action',
+ arguments: [
+ {
+ label: 'Channel',
+ key: 'channel',
+ type: 'dropdown',
+ required: true,
+ description: 'Pick a channel to send the message to.',
+ variables: false,
+ source: {
+ type: 'query',
+ name: 'getData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listChannels',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Message text',
+ key: 'message',
+ type: 'string',
+ required: true,
+ description: 'The content of your new message.',
+ variables: true,
+ },
+ ],
+ },
+ {
+ key: 'testStep',
+ name: 'Test action',
+ },
+ ],
+
+ async run($: IGlobalVariable) {
+ const channelId = $.db.step.parameters.channel as string;
+ const text = $.db.step.parameters.message as string;
+
+ const message = await postMessage($, channelId, text);
+
+ return message;
+ },
+};
diff --git a/packages/backend/src/apps/slack/actions/send-a-message-to-channel/post-message.ts b/packages/backend/src/apps/slack/actions/send-a-message-to-channel/post-message.ts
new file mode 100644
index 00000000..8a499767
--- /dev/null
+++ b/packages/backend/src/apps/slack/actions/send-a-message-to-channel/post-message.ts
@@ -0,0 +1,37 @@
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+
+const postMessage = async (
+ $: IGlobalVariable,
+ channelId: string,
+ text: string
+) => {
+ const message: {
+ data: IJSONObject | null | undefined;
+ error: IJSONObject | null | undefined;
+ } = {
+ data: null,
+ error: null,
+ };
+
+ const headers = {
+ Authorization: `Bearer ${$.auth.data.accessToken}`,
+ };
+
+ const params = {
+ channel: channelId,
+ text,
+ };
+
+ const response = await $.http.post('/chat.postMessage', params, { headers });
+
+ message.error = response?.integrationError;
+ message.data = response?.data?.message;
+
+ if (response.data.ok === false) {
+ message.error = response.data;
+ }
+
+ return message;
+};
+
+export default postMessage;
diff --git a/packages/backend/src/apps/slack/actions/send-message-to-channel.ts b/packages/backend/src/apps/slack/actions/send-message-to-channel.ts
deleted file mode 100644
index e5f290fe..00000000
--- a/packages/backend/src/apps/slack/actions/send-message-to-channel.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import SlackClient from '../client';
-
-export default class SendMessageToChannel {
- client: SlackClient;
-
- constructor(client: SlackClient) {
- this.client = client;
- }
-
- async run() {
- const channelId = this.client.step.parameters.channel as string;
- const text = this.client.step.parameters.message as string;
-
- const message = await this.client.postMessageToChannel.run(channelId, text);
-
- return message;
- }
-}
diff --git a/packages/backend/src/apps/slack/assets/favicon.svg b/packages/backend/src/apps/slack/assets/favicon.svg
index 3fbb3e78..c09453bb 100644
--- a/packages/backend/src/apps/slack/assets/favicon.svg
+++ b/packages/backend/src/apps/slack/assets/favicon.svg
@@ -1,7 +1,6 @@
-
+fill="#fff"/>
\ No newline at end of file
diff --git a/packages/backend/src/apps/slack/auth/index.ts b/packages/backend/src/apps/slack/auth/index.ts
new file mode 100644
index 00000000..99c3fdf3
--- /dev/null
+++ b/packages/backend/src/apps/slack/auth/index.ts
@@ -0,0 +1,100 @@
+import verifyCredentials from './verify-credentials';
+import isStillVerified from './is-still-verified';
+
+export default {
+ fields: [
+ {
+ key: 'accessToken',
+ label: 'Access Token',
+ type: 'string',
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: 'Access token of slack that Automatisch will connect to.',
+ clickToCopy: false,
+ },
+ ],
+ authenticationSteps: [
+ {
+ step: 1,
+ type: 'mutation',
+ name: 'createConnection',
+ arguments: [
+ {
+ name: 'key',
+ value: '{key}',
+ },
+ {
+ name: 'formattedData',
+ value: null,
+ properties: [
+ {
+ name: 'accessToken',
+ value: '{fields.accessToken}',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ step: 2,
+ type: 'mutation',
+ name: 'verifyConnection',
+ arguments: [
+ {
+ name: 'id',
+ value: '{createConnection.id}',
+ },
+ ],
+ },
+ ],
+ reconnectionSteps: [
+ {
+ step: 1,
+ type: 'mutation',
+ name: 'resetConnection',
+ arguments: [
+ {
+ name: 'id',
+ value: '{connection.id}',
+ },
+ ],
+ },
+ {
+ step: 2,
+ type: 'mutation',
+ name: 'updateConnection',
+ arguments: [
+ {
+ name: 'id',
+ value: '{connection.id}',
+ },
+ {
+ name: 'formattedData',
+ value: null,
+ properties: [
+ {
+ name: 'accessToken',
+ value: '{fields.accessToken}',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ step: 3,
+ type: 'mutation',
+ name: 'verifyConnection',
+ arguments: [
+ {
+ name: 'id',
+ value: '{connection.id}',
+ },
+ ],
+ },
+ ],
+
+ verifyCredentials,
+ isStillVerified,
+};
diff --git a/packages/backend/src/apps/slack/auth/is-still-verified.ts b/packages/backend/src/apps/slack/auth/is-still-verified.ts
new file mode 100644
index 00000000..809ad202
--- /dev/null
+++ b/packages/backend/src/apps/slack/auth/is-still-verified.ts
@@ -0,0 +1,12 @@
+import verifyCredentials from './verify-credentials';
+
+const isStillVerified = async ($: any) => {
+ try {
+ await verifyCredentials($);
+ return true;
+ } catch (error) {
+ return false;
+ }
+};
+
+export default isStillVerified;
diff --git a/packages/backend/src/apps/slack/auth/verify-credentials.ts b/packages/backend/src/apps/slack/auth/verify-credentials.ts
new file mode 100644
index 00000000..0152348a
--- /dev/null
+++ b/packages/backend/src/apps/slack/auth/verify-credentials.ts
@@ -0,0 +1,34 @@
+import qs from 'qs';
+import { IGlobalVariable } from '@automatisch/types';
+
+const verifyCredentials = async ($: IGlobalVariable) => {
+ const headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ };
+
+ const stringifiedBody = qs.stringify({
+ token: $.auth.data.accessToken,
+ });
+
+ const response = await $.http.post('/auth.test', stringifiedBody, {
+ headers,
+ });
+
+ if (response.data.ok === false) {
+ throw new Error(
+ `Error occured while verifying credentials: ${response.data.error}.(More info: https://api.slack.com/methods/auth.test#errors)`
+ );
+ }
+
+ const { bot_id: botId, user: screenName } = response.data;
+
+ $.auth.set({
+ botId,
+ screenName,
+ token: $.auth.data.accessToken,
+ });
+
+ return response.data;
+};
+
+export default verifyCredentials;
diff --git a/packages/backend/src/apps/slack/authentication.ts b/packages/backend/src/apps/slack/authentication.ts
deleted file mode 100644
index b08394f9..00000000
--- a/packages/backend/src/apps/slack/authentication.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import type { IAuthentication, IJSONObject } from '@automatisch/types';
-import SlackClient from './client';
-
-export default class Authentication implements IAuthentication {
- client: SlackClient;
-
- static requestOptions: IJSONObject = {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- };
-
- constructor(client: SlackClient) {
- this.client = client;
- }
-
- async verifyCredentials() {
- const { bot_id: botId, user: screenName } =
- await this.client.verifyAccessToken.run();
-
- return {
- botId,
- screenName,
- token: this.client.connection.formattedData.accessToken,
- };
- }
-
- async isStillVerified() {
- try {
- await this.client.verifyAccessToken.run();
- return true;
- } catch (error) {
- return false;
- }
- }
-}
diff --git a/packages/backend/src/apps/slack/client/endpoints/find-messages.ts b/packages/backend/src/apps/slack/client/endpoints/find-messages.ts
deleted file mode 100644
index 50d99819..00000000
--- a/packages/backend/src/apps/slack/client/endpoints/find-messages.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import SlackClient from '../index';
-
-export default class FindMessages {
- client: SlackClient;
-
- constructor(client: SlackClient) {
- this.client = client;
- }
-
- async run(query: string, sortBy: string, sortDirection: string, count = 1) {
- const headers = {
- Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
- };
-
- const params = {
- query,
- sort: sortBy,
- sort_dir: sortDirection,
- count,
- };
-
- const response = await this.client.httpClient.get('/search.messages', {
- headers,
- params,
- });
-
- const data = response.data;
-
- if (!data.ok) {
- if (data.error === 'missing_scope') {
- throw new Error(
- `Error occured while finding messages; ${data.error}: ${data.needed}`
- );
- }
-
- throw new Error(`Error occured while finding messages; ${data.error}`);
- }
-
- const messages = data.messages.matches;
- const message = messages?.[0];
-
- return message;
- }
-}
diff --git a/packages/backend/src/apps/slack/client/endpoints/post-message-to-channel.ts b/packages/backend/src/apps/slack/client/endpoints/post-message-to-channel.ts
deleted file mode 100644
index c8f54d7e..00000000
--- a/packages/backend/src/apps/slack/client/endpoints/post-message-to-channel.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import SlackClient from '../index';
-import { IJSONObject } from '@automatisch/types';
-
-export default class PostMessageToChannel {
- client: SlackClient;
-
- constructor(client: SlackClient) {
- this.client = client;
- }
-
- async run(channelId: string, text: string) {
- const message: {
- data: IJSONObject | null;
- error: IJSONObject | null;
- } = {
- data: null,
- error: null,
- };
-
- const headers = {
- Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
- };
-
- const params = {
- channel: channelId,
- text,
- };
-
- const response = await this.client.httpClient.post(
- '/chat.postMessage',
- params,
- { headers }
- );
-
- message.error = response?.integrationError;
- message.data = response?.data?.message;
-
- if (response.data.ok === false) {
- message.error = response.data;
- }
-
- return message;
- }
-}
diff --git a/packages/backend/src/apps/slack/client/endpoints/verify-access-token.ts b/packages/backend/src/apps/slack/client/endpoints/verify-access-token.ts
deleted file mode 100644
index d5b3bfb5..00000000
--- a/packages/backend/src/apps/slack/client/endpoints/verify-access-token.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { IJSONObject } from '@automatisch/types';
-import qs from 'qs';
-import SlackClient from '../index';
-
-export default class VerifyAccessToken {
- client: SlackClient;
-
- static requestOptions: IJSONObject = {
- headers: {
- 'Content-Type': 'application/x-www-form-urlencoded',
- },
- };
-
- constructor(client: SlackClient) {
- this.client = client;
- }
-
- async run() {
- const response = await this.client.httpClient.post(
- '/auth.test',
- qs.stringify({
- token: this.client.connection.formattedData.accessToken,
- }),
- VerifyAccessToken.requestOptions
- );
-
- if (response.data.ok === false) {
- throw new Error(
- `Error occured while verifying credentials: ${response.data.error}.(More info: https://api.slack.com/methods/auth.test#errors)`
- );
- }
-
- return response.data;
- }
-}
diff --git a/packages/backend/src/apps/slack/client/index.ts b/packages/backend/src/apps/slack/client/index.ts
deleted file mode 100644
index 5e5d79e3..00000000
--- a/packages/backend/src/apps/slack/client/index.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { IFlow, IStep, IConnection } from '@automatisch/types';
-import createHttpClient, { IHttpClient } from '../../../helpers/http-client';
-import VerifyAccessToken from './endpoints/verify-access-token';
-import PostMessageToChannel from './endpoints/post-message-to-channel';
-import FindMessages from './endpoints/find-messages';
-
-export default class SlackClient {
- flow: IFlow;
- step: IStep;
- connection: IConnection;
- httpClient: IHttpClient;
-
- verifyAccessToken: VerifyAccessToken;
- postMessageToChannel: PostMessageToChannel;
- findMessages: FindMessages;
-
- static baseUrl = 'https://slack.com/api';
-
- constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
- this.connection = connection;
- this.flow = flow;
- this.step = step;
-
- this.httpClient = createHttpClient({ baseURL: SlackClient.baseUrl });
- this.verifyAccessToken = new VerifyAccessToken(this);
- this.postMessageToChannel = new PostMessageToChannel(this);
- this.findMessages = new FindMessages(this);
- }
-}
diff --git a/packages/backend/src/apps/slack/data.ts b/packages/backend/src/apps/slack/data.ts
deleted file mode 100644
index d2743b00..00000000
--- a/packages/backend/src/apps/slack/data.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import ListChannels from './data/list-channels';
-import SlackClient from './client';
-
-export default class Data {
- client: SlackClient;
- listChannels: ListChannels;
-
- constructor(client: SlackClient) {
- this.client = client;
- this.listChannels = new ListChannels(client);
- }
-}
diff --git a/packages/backend/src/apps/slack/data/list-channels.ts b/packages/backend/src/apps/slack/data/list-channels.ts
deleted file mode 100644
index e6b8593b..00000000
--- a/packages/backend/src/apps/slack/data/list-channels.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { IJSONObject } from '@automatisch/types';
-import SlackClient from '../client';
-
-export default class ListChannels {
- client: SlackClient;
-
- constructor(client: SlackClient) {
- this.client = client;
- }
-
- async run() {
- const response = await this.client.httpClient.get('/conversations.list', {
- headers: {
- Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
- },
- });
-
- if (response.data.ok === 'false') {
- throw new Error(
- `Error occured while fetching slack channels: ${response.data.error}`
- );
- }
-
- return response.data.channels.map((channel: IJSONObject) => {
- return {
- value: channel.id,
- name: channel.name,
- };
- });
- }
-}
diff --git a/packages/backend/src/apps/slack/data/list-channels/index.ts b/packages/backend/src/apps/slack/data/list-channels/index.ts
new file mode 100644
index 00000000..d9fa4ba3
--- /dev/null
+++ b/packages/backend/src/apps/slack/data/list-channels/index.ts
@@ -0,0 +1,41 @@
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+
+export default {
+ name: 'List channels',
+ key: 'listChannels',
+
+ async run($: IGlobalVariable) {
+ const channels: {
+ data: IJSONObject[];
+ error: IJSONObject | null;
+ } = {
+ data: [],
+ error: null,
+ };
+
+ const response = await $.http.get('/conversations.list', {
+ headers: {
+ Authorization: `Bearer ${$.auth.data.accessToken}`,
+ },
+ });
+
+ if (response.integrationError) {
+ channels.error = response.integrationError;
+ return channels;
+ }
+
+ if (response.data.ok === 'false') {
+ channels.error = response.data.error;
+ return channels;
+ }
+
+ channels.data = response.data.channels.map((channel: IJSONObject) => {
+ return {
+ value: channel.id,
+ name: channel.name,
+ };
+ });
+
+ return channels;
+ },
+};
diff --git a/packages/backend/src/apps/slack/index.ts b/packages/backend/src/apps/slack/index.ts
index 6736e09a..644c8135 100644
--- a/packages/backend/src/apps/slack/index.ts
+++ b/packages/backend/src/apps/slack/index.ts
@@ -1,30 +1,8 @@
-import {
- IService,
- IAuthentication,
- IConnection,
- IFlow,
- IStep,
-} from '@automatisch/types';
-import Authentication from './authentication';
-import Triggers from './triggers';
-import Actions from './actions';
-import Data from './data';
-import SlackClient from './client';
-
-export default class Slack implements IService {
- client: SlackClient;
-
- authenticationClient: IAuthentication;
- triggers: Triggers;
- actions: Actions;
- data: Data;
-
- constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
- this.client = new SlackClient(connection, flow, step);
-
- this.authenticationClient = new Authentication(this.client);
- // this.triggers = new Triggers(this.client);
- this.actions = new Actions(this.client);
- this.data = new Data(this.client);
- }
-}
+export default {
+ name: 'Slack',
+ key: 'slack',
+ iconUrl: '{BASE_URL}/apps/slack/assets/favicon.svg',
+ authDocUrl: 'https://automatisch.io/docs/connections/slack',
+ supportsConnections: true,
+ baseUrl: 'https://slack.com/api',
+};
diff --git a/packages/backend/src/apps/slack/info.json b/packages/backend/src/apps/slack/info.json
deleted file mode 100644
index 86c16f97..00000000
--- a/packages/backend/src/apps/slack/info.json
+++ /dev/null
@@ -1,277 +0,0 @@
-{
- "name": "Slack",
- "key": "slack",
- "iconUrl": "{BASE_URL}/apps/slack/assets/favicon.svg",
- "docUrl": "https://automatisch.io/docs/slack",
- "authDocUrl": "https://automatisch.io/docs/connections/slack",
- "primaryColor": "2DAAE1",
- "supportsConnections": true,
- "fields": [
- {
- "key": "accessToken",
- "label": "Access Token",
- "type": "string",
- "required": true,
- "readOnly": false,
- "value": null,
- "placeholder": null,
- "description": "Access token of slack that Automatisch will connect to.",
- "clickToCopy": false
- }
- ],
- "authenticationSteps": [
- {
- "step": 1,
- "type": "mutation",
- "name": "createConnection",
- "arguments": [
- {
- "name": "key",
- "value": "{key}"
- },
- {
- "name": "formattedData",
- "value": null,
- "properties": [
- {
- "name": "accessToken",
- "value": "{fields.accessToken}"
- }
- ]
- }
- ]
- },
- {
- "step": 2,
- "type": "mutation",
- "name": "verifyConnection",
- "arguments": [
- {
- "name": "id",
- "value": "{createConnection.id}"
- }
- ]
- }
- ],
- "reconnectionSteps": [
- {
- "step": 1,
- "type": "mutation",
- "name": "resetConnection",
- "arguments": [
- {
- "name": "id",
- "value": "{connection.id}"
- }
- ]
- },
- {
- "step": 2,
- "type": "mutation",
- "name": "updateConnection",
- "arguments": [
- {
- "name": "id",
- "value": "{connection.id}"
- },
- {
- "name": "formattedData",
- "value": null,
- "properties": [
- {
- "name": "accessToken",
- "value": "{fields.accessToken}"
- }
- ]
- }
- ]
- },
- {
- "step": 3,
- "type": "mutation",
- "name": "verifyConnection",
- "arguments": [
- {
- "name": "id",
- "value": "{connection.id}"
- }
- ]
- }
- ],
- "triggers": [
- {
- "name": "New message posted to a channel",
- "key": "newMessageToChannel",
- "pollInterval": 15,
- "description": "Triggers when a new message is posted to a channel",
- "substeps": [
- {
- "key": "chooseConnection",
- "name": "Choose connection"
- },
- {
- "key": "chooseTrigger",
- "name": "Set up a trigger",
- "arguments": [
- {
- "label": "Channel",
- "key": "channel",
- "type": "dropdown",
- "required": true,
- "variables": false,
- "source": {
- "type": "query",
- "name": "getData",
- "arguments": [
- {
- "name": "key",
- "value": "listChannels"
- }
- ]
- }
- },
- {
- "label": "Trigger for Bot Messages?",
- "key": "triggerForBotMessages",
- "type": "dropdown",
- "description": "Should this flow trigger for bot messages?",
- "required": true,
- "value": true,
- "variables": false,
- "options": [
- {
- "label": "Yes",
- "value": true
- },
- {
- "label": "No",
- "value": false
- }
- ]
- }
- ]
- },
- {
- "key": "testStep",
- "name": "Test trigger"
- }
- ]
- }
- ],
- "actions": [
- {
- "name": "Send a message to channel",
- "key": "sendMessageToChannel",
- "description": "Send a message to a specific channel you specify.",
- "substeps": [
- {
- "key": "chooseConnection",
- "name": "Choose connection"
- },
- {
- "key": "setupAction",
- "name": "Set up action",
- "arguments": [
- {
- "label": "Channel",
- "key": "channel",
- "type": "dropdown",
- "required": true,
- "description": "Pick a channel to send the message to.",
- "variables": false,
- "source": {
- "type": "query",
- "name": "getData",
- "arguments": [
- {
- "name": "key",
- "value": "listChannels"
- }
- ]
- }
- },
- {
- "label": "Message text",
- "key": "message",
- "type": "string",
- "required": true,
- "description": "The content of your new message.",
- "variables": true
- }
- ]
- },
- {
- "key": "testStep",
- "name": "Test action"
- }
- ]
- },
- {
- "name": "Find message",
- "key": "findMessage",
- "description": "Find a Slack message using the Slack Search feature.",
- "substeps": [
- {
- "key": "chooseConnection",
- "name": "Choose connection"
- },
- {
- "key": "setupAction",
- "name": "Set up action",
- "arguments": [
- {
- "label": "Search Query",
- "key": "query",
- "type": "string",
- "required": true,
- "description": "Search query to use for finding matching messages. See the Slack Search Documentation for more information on constructing a query.",
- "variables": true
- },
- {
- "label": "Sort by",
- "key": "sortBy",
- "type": "dropdown",
- "description": "Sort messages by their match strength or by their date. Default is score.",
- "required": true,
- "value": "score",
- "variables": false,
- "options": [
- {
- "label": "Match strength",
- "value": "score"
- },
- {
- "label": "Message date time",
- "value": "timestamp"
- }
- ]
- },
- {
- "label": "Sort direction",
- "key": "sortDirection",
- "type": "dropdown",
- "description": "Sort matching messages in ascending or descending order. Default is descending.",
- "required": true,
- "value": "desc",
- "variables": false,
- "options": [
- {
- "label": "Descending (newest or best match first)",
- "value": "desc"
- },
- {
- "label": "Ascending (oldest or worst match first)",
- "value": "asc"
- }
- ]
- }
- ]
- },
- {
- "key": "testStep",
- "name": "Test action"
- }
- ]
- }
- ]
-}
diff --git a/packages/backend/src/apps/slack/triggers.ts b/packages/backend/src/apps/slack/triggers.ts
deleted file mode 100644
index 8e5862e8..00000000
--- a/packages/backend/src/apps/slack/triggers.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { IJSONObject } from '@automatisch/types';
-import NewMessageToChannel from './triggers/new-message-to-channel';
-
-export default class Triggers {
- newMessageToChannel: NewMessageToChannel;
-
- constructor(connectionData: IJSONObject, parameters: IJSONObject) {
- this.newMessageToChannel = new NewMessageToChannel(
- connectionData,
- parameters
- );
- }
-}
diff --git a/packages/backend/src/apps/slack/triggers/new-message-to-channel.ts b/packages/backend/src/apps/slack/triggers/new-message-to-channel.ts
deleted file mode 100644
index af32c0d7..00000000
--- a/packages/backend/src/apps/slack/triggers/new-message-to-channel.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { IJSONObject } from '@automatisch/types';
-import axios, { AxiosInstance } from 'axios';
-
-export default class NewMessageToChannel {
- httpClient: AxiosInstance;
- parameters: IJSONObject;
- connectionData: IJSONObject;
- BASE_URL = 'https://slack.com/api';
-
- constructor(connectionData: IJSONObject, parameters: IJSONObject) {
- this.httpClient = axios.create({ baseURL: this.BASE_URL });
- this.connectionData = connectionData;
- this.parameters = parameters;
- }
-
- async run() {
- // TODO: Fix after webhook implementation.
- }
-
- async testRun() {
- const headers = {
- Authorization: `Bearer ${this.connectionData.accessToken}`,
- };
-
- const params = {
- channel: this.parameters.channel,
- };
-
- const response = await this.httpClient.get('/conversations.history', {
- headers,
- params,
- });
-
- let lastMessage;
-
- if (this.parameters.triggerForBotMessages) {
- lastMessage = response.data.messages[0];
- } else {
- lastMessage = response.data.messages.find(
- (message: IJSONObject) =>
- !Object.prototype.hasOwnProperty.call(message, 'bot_id')
- );
- }
-
- return [lastMessage];
- }
-}
diff --git a/packages/backend/src/apps/twitter/actions.ts b/packages/backend/src/apps/twitter/actions.ts
deleted file mode 100644
index 2b3aa93d..00000000
--- a/packages/backend/src/apps/twitter/actions.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import TwitterClient from './client';
-import CreateTweet from './actions/create-tweet';
-
-export default class Actions {
- client: TwitterClient;
- createTweet: CreateTweet;
-
- constructor(client: TwitterClient) {
- this.client = client;
- this.createTweet = new CreateTweet(client);
- }
-}
diff --git a/packages/backend/src/apps/twitter/actions/create-tweet.ts b/packages/backend/src/apps/twitter/actions/create-tweet.ts
deleted file mode 100644
index 482ae0b4..00000000
--- a/packages/backend/src/apps/twitter/actions/create-tweet.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import TwitterClient from '../client';
-
-export default class CreateTweet {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run() {
- const tweet = await this.client.createTweet.run(
- this.client.step.parameters.tweet as string
- );
-
- return tweet;
- }
-}
diff --git a/packages/backend/src/apps/twitter/assets/favicon.svg b/packages/backend/src/apps/twitter/assets/favicon.svg
index 752cdc8d..576611f2 100644
--- a/packages/backend/src/apps/twitter/assets/favicon.svg
+++ b/packages/backend/src/apps/twitter/assets/favicon.svg
@@ -1,4 +1,4 @@
+
\ No newline at end of file
diff --git a/packages/backend/src/apps/twitter/auth/create-auth-data.ts b/packages/backend/src/apps/twitter/auth/create-auth-data.ts
new file mode 100644
index 00000000..b8fe115f
--- /dev/null
+++ b/packages/backend/src/apps/twitter/auth/create-auth-data.ts
@@ -0,0 +1,35 @@
+import generateRequest from '../common/generate-request';
+import { IJSONObject, IField, IGlobalVariable } from '@automatisch/types';
+import { URLSearchParams } from 'url';
+
+export default async function createAuthData($: IGlobalVariable) {
+ try {
+ const oauthRedirectUrlField = $.app.auth.fields.find(
+ (field: IField) => field.key == 'oAuthRedirectUrl'
+ );
+
+ const callbackUrl = oauthRedirectUrlField.value;
+
+ const response = await generateRequest($, {
+ requestPath: '/oauth/request_token',
+ method: 'POST',
+ data: { oauth_callback: callbackUrl },
+ });
+
+ const responseData = Object.fromEntries(new URLSearchParams(response.data));
+
+ await $.auth.set({
+ url: `${$.app.baseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}`,
+ accessToken: responseData.oauth_token,
+ accessSecret: responseData.oauth_token_secret,
+ });
+ } catch (error) {
+ const errorMessages = error.response.data.errors
+ .map((error: IJSONObject) => error.message)
+ .join(' ');
+
+ throw new Error(
+ `Error occured while verifying credentials: ${errorMessages}`
+ );
+ }
+}
diff --git a/packages/backend/src/apps/twitter/auth/index.ts b/packages/backend/src/apps/twitter/auth/index.ts
new file mode 100644
index 00000000..a5690b2f
--- /dev/null
+++ b/packages/backend/src/apps/twitter/auth/index.ts
@@ -0,0 +1,219 @@
+import createAuthData from './create-auth-data';
+import verifyCredentials from './verify-credentials';
+import isStillVerified from './is-still-verified';
+
+export default {
+ fields: [
+ {
+ key: 'oAuthRedirectUrl',
+ label: 'OAuth Redirect URL',
+ type: 'string',
+ required: true,
+ readOnly: true,
+ value: '{WEB_APP_URL}/app/twitter/connections/add',
+ placeholder: null,
+ description:
+ 'When asked to input an OAuth callback or redirect URL in Twitter OAuth, enter the URL above.',
+ clickToCopy: true,
+ },
+ {
+ key: 'consumerKey',
+ label: 'API Key',
+ type: 'string',
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: null,
+ clickToCopy: false,
+ },
+ {
+ key: 'consumerSecret',
+ label: 'API Secret',
+ type: 'string',
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: null,
+ clickToCopy: false,
+ },
+ ],
+ authenticationSteps: [
+ {
+ step: 1,
+ type: 'mutation',
+ name: 'createConnection',
+ arguments: [
+ {
+ name: 'key',
+ value: '{key}',
+ },
+ {
+ name: 'formattedData',
+ value: null,
+ properties: [
+ {
+ name: 'consumerKey',
+ value: '{fields.consumerKey}',
+ },
+ {
+ name: 'consumerSecret',
+ value: '{fields.consumerSecret}',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ step: 2,
+ type: 'mutation',
+ name: 'createAuthData',
+ arguments: [
+ {
+ name: 'id',
+ value: '{createConnection.id}',
+ },
+ ],
+ },
+ {
+ step: 3,
+ type: 'openWithPopup',
+ name: 'openAuthPopup',
+ arguments: [
+ {
+ name: 'url',
+ value: '{createAuthData.url}',
+ },
+ ],
+ },
+ {
+ step: 4,
+ type: 'mutation',
+ name: 'updateConnection',
+ arguments: [
+ {
+ name: 'id',
+ value: '{createConnection.id}',
+ },
+ {
+ name: 'formattedData',
+ value: null,
+ properties: [
+ {
+ name: 'oauthVerifier',
+ value: '{openAuthPopup.oauth_verifier}',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ step: 5,
+ type: 'mutation',
+ name: 'verifyConnection',
+ arguments: [
+ {
+ name: 'id',
+ value: '{createConnection.id}',
+ },
+ ],
+ },
+ ],
+ reconnectionSteps: [
+ {
+ step: 1,
+ type: 'mutation',
+ name: 'resetConnection',
+ arguments: [
+ {
+ name: 'id',
+ value: '{connection.id}',
+ },
+ ],
+ },
+ {
+ step: 2,
+ type: 'mutation',
+ name: 'updateConnection',
+ arguments: [
+ {
+ name: 'id',
+ value: '{connection.id}',
+ },
+ {
+ name: 'formattedData',
+ value: null,
+ properties: [
+ {
+ name: 'consumerKey',
+ value: '{fields.consumerKey}',
+ },
+ {
+ name: 'consumerSecret',
+ value: '{fields.consumerSecret}',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ step: 3,
+ type: 'mutation',
+ name: 'createAuthData',
+ arguments: [
+ {
+ name: 'id',
+ value: '{connection.id}',
+ },
+ ],
+ },
+ {
+ step: 4,
+ type: 'openWithPopup',
+ name: 'openAuthPopup',
+ arguments: [
+ {
+ name: 'url',
+ value: '{createAuthData.url}',
+ },
+ ],
+ },
+ {
+ step: 5,
+ type: 'mutation',
+ name: 'updateConnection',
+ arguments: [
+ {
+ name: 'id',
+ value: '{connection.id}',
+ },
+ {
+ name: 'formattedData',
+ value: null,
+ properties: [
+ {
+ name: 'oauthVerifier',
+ value: '{openAuthPopup.oauth_verifier}',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ step: 6,
+ type: 'mutation',
+ name: 'verifyConnection',
+ arguments: [
+ {
+ name: 'id',
+ value: '{connection.id}',
+ },
+ ],
+ },
+ ],
+
+ createAuthData,
+ verifyCredentials,
+ isStillVerified,
+};
diff --git a/packages/backend/src/apps/twitter/auth/is-still-verified.ts b/packages/backend/src/apps/twitter/auth/is-still-verified.ts
new file mode 100644
index 00000000..6ecd7ecd
--- /dev/null
+++ b/packages/backend/src/apps/twitter/auth/is-still-verified.ts
@@ -0,0 +1,13 @@
+import { IGlobalVariable } from '@automatisch/types';
+import getCurrentUser from '../common/get-current-user';
+
+const isStillVerified = async ($: IGlobalVariable) => {
+ try {
+ await getCurrentUser($);
+ return true;
+ } catch (error) {
+ return false;
+ }
+};
+
+export default isStillVerified;
diff --git a/packages/backend/src/apps/twitter/auth/verify-credentials.ts b/packages/backend/src/apps/twitter/auth/verify-credentials.ts
new file mode 100644
index 00000000..4d50b42d
--- /dev/null
+++ b/packages/backend/src/apps/twitter/auth/verify-credentials.ts
@@ -0,0 +1,24 @@
+import { IGlobalVariable } from '@automatisch/types';
+import { URLSearchParams } from 'url';
+
+const verifyCredentials = async ($: IGlobalVariable) => {
+ try {
+ const response = await $.http.post(
+ `/oauth/access_token?oauth_verifier=${$.auth.data.oauthVerifier}&oauth_token=${$.auth.data.accessToken}`,
+ null
+ );
+
+ const responseData = Object.fromEntries(new URLSearchParams(response.data));
+
+ await $.auth.set({
+ accessToken: responseData.oauth_token,
+ accessSecret: responseData.oauth_token_secret,
+ userId: responseData.user_id,
+ screenName: responseData.screen_name,
+ });
+ } catch (error) {
+ throw new Error(error.response.data);
+ }
+};
+
+export default verifyCredentials;
diff --git a/packages/backend/src/apps/twitter/authentication.ts b/packages/backend/src/apps/twitter/authentication.ts
deleted file mode 100644
index 5ddb83d7..00000000
--- a/packages/backend/src/apps/twitter/authentication.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { IAuthentication, IField } from '@automatisch/types';
-import { URLSearchParams } from 'url';
-import TwitterClient from './client';
-
-export default class Authentication implements IAuthentication {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async createAuthData() {
- const appFields = this.client.connection.appData.fields.find(
- (field: IField) => field.key == 'oAuthRedirectUrl'
- );
- const callbackUrl = appFields.value;
-
- const response = await this.client.oauthRequestToken.run(callbackUrl);
- const responseData = Object.fromEntries(new URLSearchParams(response.data));
-
- return {
- url: `${TwitterClient.baseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}`,
- accessToken: responseData.oauth_token,
- accessSecret: responseData.oauth_token_secret,
- };
- }
-
- async verifyCredentials() {
- const response = await this.client.verifyAccessToken.run();
- const responseData = Object.fromEntries(new URLSearchParams(response.data));
-
- return {
- consumerKey: this.client.connection.formattedData.consumerKey as string,
- consumerSecret: this.client.connection.formattedData
- .consumerSecret as string,
- accessToken: responseData.oauth_token,
- accessSecret: responseData.oauth_token_secret,
- userId: responseData.user_id,
- screenName: responseData.screen_name,
- };
- }
-
- async isStillVerified() {
- try {
- await this.client.getCurrentUser.run();
- return true;
- } catch (error) {
- return false;
- }
- }
-}
diff --git a/packages/backend/src/apps/twitter/client/endpoints/create-tweet.ts b/packages/backend/src/apps/twitter/client/endpoints/create-tweet.ts
deleted file mode 100644
index 52eade01..00000000
--- a/packages/backend/src/apps/twitter/client/endpoints/create-tweet.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import TwitterClient from '../index';
-
-export default class CreateTweet {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run(text: string) {
- try {
- const token = {
- key: this.client.connection.formattedData.accessToken as string,
- secret: this.client.connection.formattedData.accessSecret as string,
- };
-
- const requestData = {
- url: `${TwitterClient.baseUrl}/2/tweets`,
- method: 'POST',
- };
-
- const authHeader = this.client.oauthClient.toHeader(
- this.client.oauthClient.authorize(requestData, token)
- );
-
- const response = await this.client.httpClient.post(
- `/2/tweets`,
- { text },
- { headers: { ...authHeader } }
- );
-
- const tweet = response.data.data;
-
- return tweet;
- } catch (error) {
- const errorMessage = error.response.data.detail;
- throw new Error(`Error occured while creating a tweet: ${errorMessage}`);
- }
- }
-}
diff --git a/packages/backend/src/apps/twitter/client/endpoints/get-current-user.ts b/packages/backend/src/apps/twitter/client/endpoints/get-current-user.ts
deleted file mode 100644
index 4ccaa575..00000000
--- a/packages/backend/src/apps/twitter/client/endpoints/get-current-user.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import TwitterClient from '../index';
-
-export default class GetCurrentUser {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run() {
- const token = {
- key: this.client.connection.formattedData.accessToken as string,
- secret: this.client.connection.formattedData.accessSecret as string,
- };
-
- const requestPath = '/2/users/me';
-
- const requestData = {
- url: `${TwitterClient.baseUrl}${requestPath}`,
- method: 'GET',
- };
-
- const authHeader = this.client.oauthClient.toHeader(
- this.client.oauthClient.authorize(requestData, token)
- );
-
- const response = await this.client.httpClient.get(requestPath, {
- headers: { ...authHeader },
- });
-
- const currentUser = response.data.data;
-
- return currentUser;
- }
-}
diff --git a/packages/backend/src/apps/twitter/client/endpoints/get-user-by-username.ts b/packages/backend/src/apps/twitter/client/endpoints/get-user-by-username.ts
deleted file mode 100644
index 23ac8b83..00000000
--- a/packages/backend/src/apps/twitter/client/endpoints/get-user-by-username.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { IJSONObject } from '@automatisch/types';
-import TwitterClient from '../index';
-
-export default class GetUserByUsername {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run(username: string) {
- const token = {
- key: this.client.connection.formattedData.accessToken as string,
- secret: this.client.connection.formattedData.accessSecret as string,
- };
-
- const requestPath = `/2/users/by/username/${username}`;
-
- const requestData = {
- url: `${TwitterClient.baseUrl}${requestPath}`,
- method: 'GET',
- };
-
- const authHeader = this.client.oauthClient.toHeader(
- this.client.oauthClient.authorize(requestData, token)
- );
-
- const response = await this.client.httpClient.get(requestPath, {
- headers: { ...authHeader },
- });
-
- if (response.data?.errors) {
- const errorMessages = response.data.errors
- .map((error: IJSONObject) => error.detail)
- .join(' ');
-
- throw new Error(
- `Error occured while fetching user data: ${errorMessages}`
- );
- }
-
- const user = response.data.data;
- return user;
- }
-}
diff --git a/packages/backend/src/apps/twitter/client/endpoints/get-user-followers.ts b/packages/backend/src/apps/twitter/client/endpoints/get-user-followers.ts
deleted file mode 100644
index 543a007f..00000000
--- a/packages/backend/src/apps/twitter/client/endpoints/get-user-followers.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import { IJSONObject } from '@automatisch/types';
-import { URLSearchParams } from 'url';
-import TwitterClient from '../index';
-import omitBy from 'lodash/omitBy';
-import isEmpty from 'lodash/isEmpty';
-
-export default class GetUserFollowers {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run(userId: string, lastInternalId?: string) {
- const token = {
- key: this.client.connection.formattedData.accessToken as string,
- secret: this.client.connection.formattedData.accessSecret as string,
- };
-
- let response;
- const followers: IJSONObject[] = [];
-
- do {
- const params: IJSONObject = {
- pagination_token: response?.data?.meta?.next_token,
- };
-
- const queryParams = new URLSearchParams(omitBy(params, isEmpty));
-
- const requestPath = `/2/users/${userId}/followers${
- queryParams.toString() ? `?${queryParams.toString()}` : ''
- }`;
-
- const requestData = {
- url: `${TwitterClient.baseUrl}${requestPath}`,
- method: 'GET',
- };
-
- const authHeader = this.client.oauthClient.toHeader(
- this.client.oauthClient.authorize(requestData, token)
- );
-
- response = await this.client.httpClient.get(requestPath, {
- headers: { ...authHeader },
- });
-
- if (response.data.meta.result_count > 0) {
- response.data.data.forEach((tweet: IJSONObject) => {
- if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
- followers.push(tweet);
- } else {
- return;
- }
- });
- }
- } while (response.data.meta.next_token && lastInternalId);
-
- if (response.data?.errors) {
- const errorMessages = response.data.errors
- .map((error: IJSONObject) => error.detail)
- .join(' ');
-
- throw new Error(
- `Error occured while fetching user data: ${errorMessages}`
- );
- }
-
- return followers;
- }
-}
diff --git a/packages/backend/src/apps/twitter/client/endpoints/get-user-tweets.ts b/packages/backend/src/apps/twitter/client/endpoints/get-user-tweets.ts
deleted file mode 100644
index b6343ced..00000000
--- a/packages/backend/src/apps/twitter/client/endpoints/get-user-tweets.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { IJSONObject } from '@automatisch/types';
-import { URLSearchParams } from 'url';
-import TwitterClient from '../index';
-import omitBy from 'lodash/omitBy';
-import isEmpty from 'lodash/isEmpty';
-
-export default class GetUserTweets {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run(userId: string, lastInternalId?: string) {
- const token = {
- key: this.client.connection.formattedData.accessToken as string,
- secret: this.client.connection.formattedData.accessSecret as string,
- };
-
- let response;
- const tweets: IJSONObject[] = [];
-
- do {
- const params: IJSONObject = {
- since_id: lastInternalId,
- pagination_token: response?.data?.meta?.next_token,
- };
-
- const queryParams = new URLSearchParams(omitBy(params, isEmpty));
-
- const requestPath = `/2/users/${userId}/tweets${
- queryParams.toString() ? `?${queryParams.toString()}` : ''
- }`;
-
- const requestData = {
- url: `${TwitterClient.baseUrl}${requestPath}`,
- method: 'GET',
- };
-
- const authHeader = this.client.oauthClient.toHeader(
- this.client.oauthClient.authorize(requestData, token)
- );
-
- response = await this.client.httpClient.get(requestPath, {
- headers: { ...authHeader },
- });
-
- if (response.data.meta.result_count > 0) {
- response.data.data.forEach((tweet: IJSONObject) => {
- if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
- tweets.push(tweet);
- } else {
- return;
- }
- });
- }
- } while (response.data.meta.next_token && lastInternalId);
-
- if (response.data?.errors) {
- const errorMessages = response.data.errors
- .map((error: IJSONObject) => error.detail)
- .join(' ');
-
- throw new Error(
- `Error occured while fetching user data: ${errorMessages}`
- );
- }
-
- return tweets;
- }
-}
diff --git a/packages/backend/src/apps/twitter/client/endpoints/oauth-request-token.ts b/packages/backend/src/apps/twitter/client/endpoints/oauth-request-token.ts
deleted file mode 100644
index 6ef9d899..00000000
--- a/packages/backend/src/apps/twitter/client/endpoints/oauth-request-token.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { IJSONObject } from '@automatisch/types';
-import TwitterClient from '../index';
-
-export default class OAuthRequestToken {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run(callbackUrl: string) {
- try {
- const requestData = {
- url: `${TwitterClient.baseUrl}/oauth/request_token`,
- method: 'POST',
- data: { oauth_callback: callbackUrl },
- };
-
- const authHeader = this.client.oauthClient.toHeader(
- this.client.oauthClient.authorize(requestData)
- );
-
- const response = await this.client.httpClient.post(
- `/oauth/request_token`,
- null,
- {
- headers: { ...authHeader },
- }
- );
-
- return response;
- } catch (error) {
- const errorMessages = error.response.data.errors
- .map((error: IJSONObject) => error.message)
- .join(' ');
-
- throw new Error(
- `Error occured while verifying credentials: ${errorMessages}`
- );
- }
- }
-}
diff --git a/packages/backend/src/apps/twitter/client/endpoints/search-tweets.ts b/packages/backend/src/apps/twitter/client/endpoints/search-tweets.ts
deleted file mode 100644
index 89eddeba..00000000
--- a/packages/backend/src/apps/twitter/client/endpoints/search-tweets.ts
+++ /dev/null
@@ -1,74 +0,0 @@
-import { IJSONObject } from '@automatisch/types';
-import { URLSearchParams } from 'url';
-import TwitterClient from '../index';
-import omitBy from 'lodash/omitBy';
-import isEmpty from 'lodash/isEmpty';
-import qs from 'qs';
-
-export default class SearchTweets {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run(searchTerm: string, lastInternalId?: string) {
- const token = {
- key: this.client.connection.formattedData.accessToken as string,
- secret: this.client.connection.formattedData.accessSecret as string,
- };
-
- let response;
- const tweets: {
- data: IJSONObject[];
- error: IJSONObject | null;
- } = {
- data: [],
- error: null,
- };
-
- do {
- const params: IJSONObject = {
- query: searchTerm,
- since_id: lastInternalId,
- pagination_token: response?.data?.meta?.next_token,
- };
-
- const queryParams = qs.stringify(omitBy(params, isEmpty));
-
- const requestPath = `/2/tweets/search/recent${
- queryParams.toString() ? `?${queryParams.toString()}` : ''
- }`;
-
- const requestData = {
- url: `${TwitterClient.baseUrl}${requestPath}`,
- method: 'GET',
- };
-
- const authHeader = this.client.oauthClient.toHeader(
- this.client.oauthClient.authorize(requestData, token)
- );
-
- response = await this.client.httpClient.get(requestPath, {
- headers: { ...authHeader },
- });
-
- if (response.integrationError) {
- tweets.error = response.integrationError;
- return tweets;
- }
-
- if (response.data.meta.result_count > 0) {
- response.data.data.forEach((tweet: IJSONObject) => {
- if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
- tweets.data.push(tweet);
- } else {
- return;
- }
- });
- }
- } while (response.data.meta.next_token && lastInternalId);
-
- return tweets;
- }
-}
diff --git a/packages/backend/src/apps/twitter/client/endpoints/verify-access-token.ts b/packages/backend/src/apps/twitter/client/endpoints/verify-access-token.ts
deleted file mode 100644
index 7c51a52f..00000000
--- a/packages/backend/src/apps/twitter/client/endpoints/verify-access-token.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import TwitterClient from '../index';
-
-export default class VerifyAccessToken {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run() {
- try {
- return await this.client.httpClient.post(
- `/oauth/access_token?oauth_verifier=${this.client.connection.formattedData.oauthVerifier}&oauth_token=${this.client.connection.formattedData.accessToken}`,
- null
- );
- } catch (error) {
- throw new Error(error.response.data);
- }
- }
-}
diff --git a/packages/backend/src/apps/twitter/client/index.ts b/packages/backend/src/apps/twitter/client/index.ts
deleted file mode 100644
index 4cb899b5..00000000
--- a/packages/backend/src/apps/twitter/client/index.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { IFlow, IStep, IConnection } from '@automatisch/types';
-import OAuth from 'oauth-1.0a';
-import crypto from 'crypto';
-import createHttpClient, { IHttpClient } from '../../../helpers/http-client';
-import OAuthRequestToken from './endpoints/oauth-request-token';
-import VerifyAccessToken from './endpoints/verify-access-token';
-import GetCurrentUser from './endpoints/get-current-user';
-import GetUserByUsername from './endpoints/get-user-by-username';
-import GetUserTweets from './endpoints/get-user-tweets';
-import CreateTweet from './endpoints/create-tweet';
-import SearchTweets from './endpoints/search-tweets';
-import GetUserFollowers from './endpoints/get-user-followers';
-
-export default class TwitterClient {
- flow: IFlow;
- step: IStep;
- connection: IConnection;
- oauthClient: OAuth;
- httpClient: IHttpClient;
-
- oauthRequestToken: OAuthRequestToken;
- verifyAccessToken: VerifyAccessToken;
- getCurrentUser: GetCurrentUser;
- getUserByUsername: GetUserByUsername;
- getUserTweets: GetUserTweets;
- createTweet: CreateTweet;
- searchTweets: SearchTweets;
- getUserFollowers: GetUserFollowers;
-
- static baseUrl = 'https://api.twitter.com';
-
- constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
- this.connection = connection;
- this.flow = flow;
- this.step = step;
-
- this.httpClient = createHttpClient({ baseURL: TwitterClient.baseUrl });
-
- const consumerData = {
- key: this.connection.formattedData.consumerKey as string,
- secret: this.connection.formattedData.consumerSecret as string,
- };
-
- this.oauthClient = new OAuth({
- consumer: consumerData,
- signature_method: 'HMAC-SHA1',
- hash_function(base_string, key) {
- return crypto
- .createHmac('sha1', key)
- .update(base_string)
- .digest('base64');
- },
- });
-
- this.oauthRequestToken = new OAuthRequestToken(this);
- this.verifyAccessToken = new VerifyAccessToken(this);
- this.getCurrentUser = new GetCurrentUser(this);
- this.getUserByUsername = new GetUserByUsername(this);
- this.getUserTweets = new GetUserTweets(this);
- this.createTweet = new CreateTweet(this);
- this.searchTweets = new SearchTweets(this);
- this.getUserFollowers = new GetUserFollowers(this);
- }
-}
diff --git a/packages/backend/src/apps/twitter/common/generate-request.ts b/packages/backend/src/apps/twitter/common/generate-request.ts
new file mode 100644
index 00000000..8772b997
--- /dev/null
+++ b/packages/backend/src/apps/twitter/common/generate-request.ts
@@ -0,0 +1,44 @@
+import { Token } from 'oauth-1.0a';
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+import oauthClient from './oauth-client';
+import { Method } from 'axios';
+
+type IGenereateRequestOptons = {
+ requestPath: string;
+ method: string;
+ data?: IJSONObject;
+};
+
+const generateRequest = async (
+ $: IGlobalVariable,
+ options: IGenereateRequestOptons
+) => {
+ const { requestPath, method, data } = options;
+
+ const token: Token = {
+ key: $.auth.data.accessToken as string,
+ secret: $.auth.data.accessSecret as string,
+ };
+
+ const requestData = {
+ url: `${$.app.baseUrl}${requestPath}`,
+ method,
+ data,
+ };
+
+ const authHeader = oauthClient($).toHeader(
+ oauthClient($).authorize(requestData, token)
+ );
+
+ const response = await $.http.request({
+ url: requestData.url,
+ method: requestData.method as Method,
+ headers: {
+ ...authHeader,
+ },
+ });
+
+ return response;
+};
+
+export default generateRequest;
diff --git a/packages/backend/src/apps/twitter/common/get-current-user.ts b/packages/backend/src/apps/twitter/common/get-current-user.ts
new file mode 100644
index 00000000..0823192f
--- /dev/null
+++ b/packages/backend/src/apps/twitter/common/get-current-user.ts
@@ -0,0 +1,14 @@
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+import generateRequest from './generate-request';
+
+const getCurrentUser = async ($: IGlobalVariable): Promise => {
+ const response = await generateRequest($, {
+ requestPath: '/2/users/me',
+ method: 'GET',
+ });
+
+ const currentUser = response.data.data;
+ return currentUser;
+};
+
+export default getCurrentUser;
diff --git a/packages/backend/src/apps/twitter/common/get-user-by-username.ts b/packages/backend/src/apps/twitter/common/get-user-by-username.ts
new file mode 100644
index 00000000..45e108be
--- /dev/null
+++ b/packages/backend/src/apps/twitter/common/get-user-by-username.ts
@@ -0,0 +1,22 @@
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+import generateRequest from './generate-request';
+
+const getUserByUsername = async ($: IGlobalVariable, username: string) => {
+ const response = await generateRequest($, {
+ requestPath: `/2/users/by/username/${username}`,
+ method: 'GET',
+ });
+
+ if (response.data.errors) {
+ const errorMessages = response.data.errors
+ .map((error: IJSONObject) => error.detail)
+ .join(' ');
+
+ throw new Error(`Error occured while fetching user data: ${errorMessages}`);
+ }
+
+ const user = response.data.data;
+ return user;
+};
+
+export default getUserByUsername;
diff --git a/packages/backend/src/apps/twitter/common/get-user-followers.ts b/packages/backend/src/apps/twitter/common/get-user-followers.ts
new file mode 100644
index 00000000..0d4e1e64
--- /dev/null
+++ b/packages/backend/src/apps/twitter/common/get-user-followers.ts
@@ -0,0 +1,68 @@
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+import { URLSearchParams } from 'url';
+import { omitBy, isEmpty } from 'lodash';
+import generateRequest from './generate-request';
+
+type GetUserFollowersOptions = {
+ userId: string;
+ lastInternalId?: string;
+};
+
+const getUserFollowers = async (
+ $: IGlobalVariable,
+ options: GetUserFollowersOptions
+) => {
+ let response;
+
+ const followers: {
+ data: IJSONObject[];
+ error: IJSONObject | null;
+ } = {
+ data: [],
+ error: null,
+ };
+
+ do {
+ const params: IJSONObject = {
+ pagination_token: response?.data?.meta?.next_token,
+ };
+
+ const queryParams = new URLSearchParams(omitBy(params, isEmpty));
+
+ const requestPath = `/2/users/${options.userId}/followers${
+ queryParams.toString() ? `?${queryParams.toString()}` : ''
+ }`;
+
+ response = await generateRequest($, {
+ requestPath,
+ method: 'GET',
+ });
+
+ if (response.integrationError) {
+ followers.error = response.integrationError;
+ return followers;
+ }
+
+ if (response.data?.errors) {
+ followers.error = response.data.errors;
+ return followers;
+ }
+
+ if (response.data.meta.result_count > 0) {
+ response.data.data.forEach((tweet: IJSONObject) => {
+ if (
+ !options.lastInternalId ||
+ Number(tweet.id) > Number(options.lastInternalId)
+ ) {
+ followers.data.push(tweet);
+ } else {
+ return;
+ }
+ });
+ }
+ } while (response.data.meta.next_token && options.lastInternalId);
+
+ return followers;
+};
+
+export default getUserFollowers;
diff --git a/packages/backend/src/apps/twitter/common/get-user-tweets.ts b/packages/backend/src/apps/twitter/common/get-user-tweets.ts
new file mode 100644
index 00000000..901d22a3
--- /dev/null
+++ b/packages/backend/src/apps/twitter/common/get-user-tweets.ts
@@ -0,0 +1,79 @@
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+import { URLSearchParams } from 'url';
+import omitBy from 'lodash/omitBy';
+import isEmpty from 'lodash/isEmpty';
+import generateRequest from './generate-request';
+import getCurrentUser from './get-current-user';
+import getUserByUsername from './get-user-by-username';
+
+type IGetUserTweetsOptions = {
+ currentUser: boolean;
+ userId?: string;
+ lastInternalId?: string;
+};
+
+const getUserTweets = async (
+ $: IGlobalVariable,
+ options: IGetUserTweetsOptions
+) => {
+ let username: string;
+
+ if (options.currentUser) {
+ const currentUser = await getCurrentUser($);
+ username = currentUser.username as string;
+ } else {
+ username = $.db.step.parameters.username as string;
+ }
+
+ const user = await getUserByUsername($, username);
+
+ let response;
+
+ const tweets: {
+ data: IJSONObject[];
+ error: IJSONObject | null;
+ } = {
+ data: [],
+ error: null,
+ };
+
+ do {
+ const params: IJSONObject = {
+ since_id: options.lastInternalId,
+ pagination_token: response?.data?.meta?.next_token,
+ };
+
+ const queryParams = new URLSearchParams(omitBy(params, isEmpty));
+
+ const requestPath = `/2/users/${user.id}/tweets${
+ queryParams.toString() ? `?${queryParams.toString()}` : ''
+ }`;
+
+ response = await generateRequest($, {
+ requestPath,
+ method: 'GET',
+ });
+
+ if (response.integrationError) {
+ tweets.error = response.integrationError;
+ return tweets;
+ }
+
+ if (response.data.meta.result_count > 0) {
+ response.data.data.forEach((tweet: IJSONObject) => {
+ if (
+ !options.lastInternalId ||
+ Number(tweet.id) > Number(options.lastInternalId)
+ ) {
+ tweets.data.push(tweet);
+ } else {
+ return;
+ }
+ });
+ }
+ } while (response.data.meta.next_token && options.lastInternalId);
+
+ return tweets;
+};
+
+export default getUserTweets;
diff --git a/packages/backend/src/apps/twitter/common/oauth-client.ts b/packages/backend/src/apps/twitter/common/oauth-client.ts
new file mode 100644
index 00000000..c29f6a0a
--- /dev/null
+++ b/packages/backend/src/apps/twitter/common/oauth-client.ts
@@ -0,0 +1,23 @@
+import { IGlobalVariable } from '@automatisch/types';
+import crypto from 'crypto';
+import OAuth from 'oauth-1.0a';
+
+const oauthClient = ($: IGlobalVariable) => {
+ const consumerData = {
+ key: $.auth.data.consumerKey as string,
+ secret: $.auth.data.consumerSecret as string,
+ };
+
+ return new OAuth({
+ consumer: consumerData,
+ signature_method: 'HMAC-SHA1',
+ hash_function(base_string, key) {
+ return crypto
+ .createHmac('sha1', key)
+ .update(base_string)
+ .digest('base64');
+ },
+ });
+};
+
+export default oauthClient;
diff --git a/packages/backend/src/apps/twitter/index.ts b/packages/backend/src/apps/twitter/index.ts
index ee8e1005..0f0ef9a4 100644
--- a/packages/backend/src/apps/twitter/index.ts
+++ b/packages/backend/src/apps/twitter/index.ts
@@ -1,27 +1,8 @@
-import {
- IService,
- IAuthentication,
- IFlow,
- IStep,
- IConnection,
-} from '@automatisch/types';
-import Authentication from './authentication';
-import Triggers from './triggers';
-import Actions from './actions';
-import TwitterClient from './client';
-
-export default class Twitter implements IService {
- client: TwitterClient;
-
- authenticationClient: IAuthentication;
- triggers: Triggers;
- actions: Actions;
-
- constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
- this.client = new TwitterClient(connection, flow, step);
-
- this.authenticationClient = new Authentication(this.client);
- this.triggers = new Triggers(this.client);
- this.actions = new Actions(this.client);
- }
-}
+export default {
+ name: 'Twitter',
+ key: 'twitter',
+ iconUrl: '{BASE_URL}/apps/twitter/assets/favicon.svg',
+ authDocUrl: 'https://automatisch.io/docs/connections/twitter',
+ supportsConnections: true,
+ baseUrl: 'https://api.twitter.com',
+};
diff --git a/packages/backend/src/apps/twitter/info.json b/packages/backend/src/apps/twitter/info.json
deleted file mode 100644
index a9a4cd6c..00000000
--- a/packages/backend/src/apps/twitter/info.json
+++ /dev/null
@@ -1,338 +0,0 @@
-{
- "name": "Twitter",
- "key": "twitter",
- "iconUrl": "{BASE_URL}/apps/twitter/assets/favicon.svg",
- "docUrl": "https://automatisch.io/docs/twitter",
- "authDocUrl": "https://automatisch.io/docs/connections/twitter",
- "primaryColor": "2DAAE1",
- "supportsConnections": true,
- "fields": [
- {
- "key": "oAuthRedirectUrl",
- "label": "OAuth Redirect URL",
- "type": "string",
- "required": true,
- "readOnly": true,
- "value": "{WEB_APP_URL}/app/twitter/connections/add",
- "placeholder": null,
- "description": "When asked to input an OAuth callback or redirect URL in Twitter OAuth, enter the URL above.",
- "clickToCopy": true
- },
- {
- "key": "consumerKey",
- "label": "API Key",
- "type": "string",
- "required": true,
- "readOnly": false,
- "value": null,
- "placeholder": null,
- "description": null,
- "clickToCopy": false
- },
- {
- "key": "consumerSecret",
- "label": "API Secret",
- "type": "string",
- "required": true,
- "readOnly": false,
- "value": null,
- "placeholder": null,
- "description": null,
- "clickToCopy": false
- }
- ],
- "authenticationSteps": [
- {
- "step": 1,
- "type": "mutation",
- "name": "createConnection",
- "arguments": [
- {
- "name": "key",
- "value": "{key}"
- },
- {
- "name": "formattedData",
- "value": null,
- "properties": [
- {
- "name": "consumerKey",
- "value": "{fields.consumerKey}"
- },
- {
- "name": "consumerSecret",
- "value": "{fields.consumerSecret}"
- }
- ]
- }
- ]
- },
- {
- "step": 2,
- "type": "mutation",
- "name": "createAuthData",
- "arguments": [
- {
- "name": "id",
- "value": "{createConnection.id}"
- }
- ]
- },
- {
- "step": 3,
- "type": "openWithPopup",
- "name": "openAuthPopup",
- "arguments": [
- {
- "name": "url",
- "value": "{createAuthData.url}"
- }
- ]
- },
- {
- "step": 4,
- "type": "mutation",
- "name": "updateConnection",
- "arguments": [
- {
- "name": "id",
- "value": "{createConnection.id}"
- },
- {
- "name": "formattedData",
- "value": null,
- "properties": [
- {
- "name": "oauthVerifier",
- "value": "{openAuthPopup.oauth_verifier}"
- }
- ]
- }
- ]
- },
- {
- "step": 5,
- "type": "mutation",
- "name": "verifyConnection",
- "arguments": [
- {
- "name": "id",
- "value": "{createConnection.id}"
- }
- ]
- }
- ],
- "reconnectionSteps": [
- {
- "step": 1,
- "type": "mutation",
- "name": "resetConnection",
- "arguments": [
- {
- "name": "id",
- "value": "{connection.id}"
- }
- ]
- },
- {
- "step": 2,
- "type": "mutation",
- "name": "updateConnection",
- "arguments": [
- {
- "name": "id",
- "value": "{connection.id}"
- },
- {
- "name": "formattedData",
- "value": null,
- "properties": [
- {
- "name": "consumerKey",
- "value": "{fields.consumerKey}"
- },
- {
- "name": "consumerSecret",
- "value": "{fields.consumerSecret}"
- }
- ]
- }
- ]
- },
- {
- "step": 3,
- "type": "mutation",
- "name": "createAuthData",
- "arguments": [
- {
- "name": "id",
- "value": "{connection.id}"
- }
- ]
- },
- {
- "step": 4,
- "type": "openWithPopup",
- "name": "openAuthPopup",
- "arguments": [
- {
- "name": "url",
- "value": "{createAuthData.url}"
- }
- ]
- },
- {
- "step": 5,
- "type": "mutation",
- "name": "updateConnection",
- "arguments": [
- {
- "name": "id",
- "value": "{connection.id}"
- },
- {
- "name": "formattedData",
- "value": null,
- "properties": [
- {
- "name": "oauthVerifier",
- "value": "{openAuthPopup.oauth_verifier}"
- }
- ]
- }
- ]
- },
- {
- "step": 6,
- "type": "mutation",
- "name": "verifyConnection",
- "arguments": [
- {
- "name": "id",
- "value": "{connection.id}"
- }
- ]
- }
- ],
- "triggers": [
- {
- "name": "My Tweets",
- "key": "myTweets",
- "pollInterval": 15,
- "description": "Will be triggered when you tweet something new.",
- "substeps": [
- {
- "key": "chooseConnection",
- "name": "Choose connection"
- },
- {
- "key": "testStep",
- "name": "Test trigger"
- }
- ]
- },
- {
- "name": "User Tweets",
- "key": "userTweets",
- "pollInterval": 15,
- "description": "Will be triggered when a specific user tweet something new.",
- "substeps": [
- {
- "key": "chooseConnection",
- "name": "Choose connection"
- },
- {
- "key": "chooseTrigger",
- "name": "Set up a trigger",
- "arguments": [
- {
- "label": "Username",
- "key": "username",
- "type": "string",
- "required": true
- }
- ]
- },
- {
- "key": "testStep",
- "name": "Test trigger"
- }
- ]
- },
- {
- "name": "Search Tweets",
- "key": "searchTweets",
- "pollInterval": 15,
- "description": "Will be triggered when any user tweet something containing a specific keyword, phrase, username or hashtag.",
- "substeps": [
- {
- "key": "chooseConnection",
- "name": "Choose connection"
- },
- {
- "key": "chooseTrigger",
- "name": "Set up a trigger",
- "arguments": [
- {
- "label": "Search Term",
- "key": "searchTerm",
- "type": "string",
- "required": true
- }
- ]
- },
- {
- "key": "testStep",
- "name": "Test trigger"
- }
- ]
- },
- {
- "name": "New follower of me",
- "key": "myFollowers",
- "pollInterval": 15,
- "description": "Will be triggered when you have a new follower.",
- "substeps": [
- {
- "key": "chooseConnection",
- "name": "Choose connection"
- },
- {
- "key": "testStep",
- "name": "Test trigger"
- }
- ]
- }
- ],
- "actions": [
- {
- "name": "Create Tweet",
- "key": "createTweet",
- "description": "Will create a tweet.",
- "substeps": [
- {
- "key": "chooseConnection",
- "name": "Choose connection"
- },
- {
- "key": "chooseAction",
- "name": "Set up action",
- "arguments": [
- {
- "label": "Tweet body",
- "key": "tweet",
- "type": "string",
- "required": true,
- "description": "The content of your new tweet.",
- "variables": true
- }
- ]
- },
- {
- "key": "testStep",
- "name": "Test action"
- }
- ]
- }
- ]
-}
diff --git a/packages/backend/src/apps/twitter/triggers.ts b/packages/backend/src/apps/twitter/triggers.ts
deleted file mode 100644
index 2d611581..00000000
--- a/packages/backend/src/apps/twitter/triggers.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import TwitterClient from './client';
-import UserTweets from './triggers/user-tweets';
-import SearchTweets from './triggers/search-tweets';
-import MyTweets from './triggers/my-tweets';
-import MyFollowers from './triggers/my-followers';
-
-export default class Triggers {
- client: TwitterClient;
- userTweets: UserTweets;
- searchTweets: SearchTweets;
- myTweets: MyTweets;
- myFollowers: MyFollowers;
-
- constructor(client: TwitterClient) {
- this.client = client;
- this.userTweets = new UserTweets(client);
- this.searchTweets = new SearchTweets(client);
- this.myTweets = new MyTweets(client);
- this.myFollowers = new MyFollowers(client);
- }
-}
diff --git a/packages/backend/src/apps/twitter/triggers/my-followers.ts b/packages/backend/src/apps/twitter/triggers/my-followers.ts
deleted file mode 100644
index 8aaaba51..00000000
--- a/packages/backend/src/apps/twitter/triggers/my-followers.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import TwitterClient from '../client';
-
-export default class MyFollowers {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run(lastInternalId: string) {
- return this.getFollowers(lastInternalId);
- }
-
- async testRun() {
- return this.getFollowers();
- }
-
- async getFollowers(lastInternalId?: string) {
- const { username } = await this.client.getCurrentUser.run();
- const user = await this.client.getUserByUsername.run(username as string);
-
- const tweets = await this.client.getUserFollowers.run(
- user.id,
- lastInternalId
- );
- return tweets;
- }
-}
diff --git a/packages/backend/src/apps/twitter/triggers/my-tweets.ts b/packages/backend/src/apps/twitter/triggers/my-tweets.ts
deleted file mode 100644
index dd88b69c..00000000
--- a/packages/backend/src/apps/twitter/triggers/my-tweets.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import TwitterClient from '../client';
-
-export default class MyTweets {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run(lastInternalId: string) {
- return this.getTweets(lastInternalId);
- }
-
- async testRun() {
- return this.getTweets();
- }
-
- async getTweets(lastInternalId?: string) {
- const { username } = await this.client.getCurrentUser.run();
- const user = await this.client.getUserByUsername.run(username as string);
-
- const tweets = await this.client.getUserTweets.run(user.id, lastInternalId);
- return tweets;
- }
-}
diff --git a/packages/backend/src/apps/twitter/triggers/my-tweets/index.ts b/packages/backend/src/apps/twitter/triggers/my-tweets/index.ts
new file mode 100644
index 00000000..e6cb95a0
--- /dev/null
+++ b/packages/backend/src/apps/twitter/triggers/my-tweets/index.ts
@@ -0,0 +1,30 @@
+import { IGlobalVariable } from '@automatisch/types';
+import getUserTweets from '../../common/get-user-tweets';
+
+export default {
+ name: 'My Tweets',
+ key: 'myTweets',
+ pollInterval: 15,
+ description: 'Will be triggered when you tweet something new.',
+ substeps: [
+ {
+ key: 'chooseConnection',
+ name: 'Choose connection',
+ },
+ {
+ key: 'testStep',
+ name: 'Test trigger',
+ },
+ ],
+
+ async run($: IGlobalVariable) {
+ return await getUserTweets($, {
+ currentUser: true,
+ lastInternalId: $.db.flow.lastInternalId,
+ });
+ },
+
+ async testRun($: IGlobalVariable) {
+ return await getUserTweets($, { currentUser: true });
+ },
+};
diff --git a/packages/backend/src/apps/twitter/triggers/new-follower-of-me/index.ts b/packages/backend/src/apps/twitter/triggers/new-follower-of-me/index.ts
new file mode 100644
index 00000000..c9b4be84
--- /dev/null
+++ b/packages/backend/src/apps/twitter/triggers/new-follower-of-me/index.ts
@@ -0,0 +1,27 @@
+import { IGlobalVariable } from '@automatisch/types';
+import myFollowers from './my-followers';
+
+export default {
+ name: 'New follower of me',
+ key: 'myFollowers',
+ pollInterval: 15,
+ description: 'Will be triggered when you have a new follower.',
+ substeps: [
+ {
+ key: 'chooseConnection',
+ name: 'Choose connection',
+ },
+ {
+ key: 'testStep',
+ name: 'Test trigger',
+ },
+ ],
+
+ async run($: IGlobalVariable) {
+ return await myFollowers($, $.db.flow.lastInternalId);
+ },
+
+ async testRun($: IGlobalVariable) {
+ return await myFollowers($);
+ },
+};
diff --git a/packages/backend/src/apps/twitter/triggers/new-follower-of-me/my-followers.ts b/packages/backend/src/apps/twitter/triggers/new-follower-of-me/my-followers.ts
new file mode 100644
index 00000000..a4d0fa0e
--- /dev/null
+++ b/packages/backend/src/apps/twitter/triggers/new-follower-of-me/my-followers.ts
@@ -0,0 +1,17 @@
+import { IGlobalVariable } from '@automatisch/types';
+import getCurrentUser from '../../common/get-current-user';
+import getUserByUsername from '../../common/get-user-by-username';
+import getUserFollowers from '../../common/get-user-followers';
+
+const myFollowers = async ($: IGlobalVariable, lastInternalId?: string) => {
+ const { username } = await getCurrentUser($);
+ const user = await getUserByUsername($, username as string);
+
+ const tweets = await getUserFollowers($, {
+ userId: user.id,
+ lastInternalId,
+ });
+ return tweets;
+};
+
+export default myFollowers;
diff --git a/packages/backend/src/apps/twitter/triggers/search-tweets.ts b/packages/backend/src/apps/twitter/triggers/search-tweets.ts
deleted file mode 100644
index 44ccb5df..00000000
--- a/packages/backend/src/apps/twitter/triggers/search-tweets.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import TwitterClient from '../client';
-
-export default class SearchTweets {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run(lastInternalId: string) {
- return this.getTweets(lastInternalId);
- }
-
- async testRun() {
- return this.getTweets();
- }
-
- async getTweets(lastInternalId?: string) {
- const tweets = await this.client.searchTweets.run(
- this.client.step.parameters.searchTerm as string,
- lastInternalId
- );
-
- return tweets;
- }
-}
diff --git a/packages/backend/src/apps/twitter/triggers/search-tweets/index.ts b/packages/backend/src/apps/twitter/triggers/search-tweets/index.ts
new file mode 100644
index 00000000..0f67d908
--- /dev/null
+++ b/packages/backend/src/apps/twitter/triggers/search-tweets/index.ts
@@ -0,0 +1,45 @@
+import { IGlobalVariable } from '@automatisch/types';
+import searchTweets from './search-tweets';
+
+export default {
+ name: 'Search Tweets',
+ key: 'searchTweets',
+ pollInterval: 15,
+ description:
+ 'Will be triggered when any user tweet something containing a specific keyword, phrase, username or hashtag.',
+ substeps: [
+ {
+ key: 'chooseConnection',
+ name: 'Choose connection',
+ },
+ {
+ key: 'chooseTrigger',
+ name: 'Set up a trigger',
+ arguments: [
+ {
+ label: 'Search Term',
+ key: 'searchTerm',
+ type: 'string',
+ required: true,
+ },
+ ],
+ },
+ {
+ key: 'testStep',
+ name: 'Test trigger',
+ },
+ ],
+
+ async run($: IGlobalVariable) {
+ return await searchTweets($, {
+ searchTerm: $.db.step.parameters.searchTerm as string,
+ lastInternalId: $.db.flow.lastInternalId,
+ });
+ },
+
+ async testRun($: IGlobalVariable) {
+ return await searchTweets($, {
+ searchTerm: $.db.step.parameters.searchTerm as string,
+ });
+ },
+};
diff --git a/packages/backend/src/apps/twitter/triggers/search-tweets/search-tweets.ts b/packages/backend/src/apps/twitter/triggers/search-tweets/search-tweets.ts
new file mode 100644
index 00000000..8e3886c6
--- /dev/null
+++ b/packages/backend/src/apps/twitter/triggers/search-tweets/search-tweets.ts
@@ -0,0 +1,70 @@
+import { IGlobalVariable, IJSONObject } from '@automatisch/types';
+import qs from 'qs';
+import generateRequest from '../../common/generate-request';
+import { omitBy, isEmpty } from 'lodash';
+
+type ISearchTweetsOptions = {
+ searchTerm: string;
+ lastInternalId?: string;
+};
+
+const searchTweets = async (
+ $: IGlobalVariable,
+ options: ISearchTweetsOptions
+) => {
+ let response;
+
+ const tweets: {
+ data: IJSONObject[];
+ error: IJSONObject | null;
+ } = {
+ data: [],
+ error: null,
+ };
+
+ do {
+ const params: IJSONObject = {
+ query: options.searchTerm,
+ since_id: options.lastInternalId,
+ pagination_token: response?.data?.meta?.next_token,
+ };
+
+ const queryParams = qs.stringify(omitBy(params, isEmpty));
+
+ const requestPath = `/2/tweets/search/recent${
+ queryParams.toString() ? `?${queryParams.toString()}` : ''
+ }`;
+
+ response = await generateRequest($, {
+ requestPath,
+ method: 'GET',
+ });
+
+ if (response.integrationError) {
+ tweets.error = response.integrationError;
+ return tweets;
+ }
+
+ if (response.data.errors) {
+ tweets.error = response.data.errors;
+ return tweets;
+ }
+
+ if (response.data.meta.result_count > 0) {
+ response.data.data.forEach((tweet: IJSONObject) => {
+ if (
+ !options.lastInternalId ||
+ Number(tweet.id) > Number(options.lastInternalId)
+ ) {
+ tweets.data.push(tweet);
+ } else {
+ return;
+ }
+ });
+ }
+ } while (response.data.meta.next_token && options.lastInternalId);
+
+ return tweets;
+};
+
+export default searchTweets;
diff --git a/packages/backend/src/apps/twitter/triggers/user-tweets.ts b/packages/backend/src/apps/twitter/triggers/user-tweets.ts
deleted file mode 100644
index bd4919b0..00000000
--- a/packages/backend/src/apps/twitter/triggers/user-tweets.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import TwitterClient from '../client';
-
-export default class UserTweets {
- client: TwitterClient;
-
- constructor(client: TwitterClient) {
- this.client = client;
- }
-
- async run(lastInternalId: string) {
- return this.getTweets(lastInternalId);
- }
-
- async testRun() {
- return this.getTweets();
- }
-
- async getTweets(lastInternalId?: string) {
- const user = await this.client.getUserByUsername.run(
- this.client.step.parameters.username as string
- );
-
- const tweets = await this.client.getUserTweets.run(user.id, lastInternalId);
-
- return tweets;
- }
-}
diff --git a/packages/backend/src/apps/twitter/triggers/user-tweets/index.ts b/packages/backend/src/apps/twitter/triggers/user-tweets/index.ts
new file mode 100644
index 00000000..23517c9e
--- /dev/null
+++ b/packages/backend/src/apps/twitter/triggers/user-tweets/index.ts
@@ -0,0 +1,46 @@
+import { IGlobalVariable } from '@automatisch/types';
+import getUserTweets from '../../common/get-user-tweets';
+
+export default {
+ name: 'User Tweets',
+ key: 'userTweets',
+ pollInterval: 15,
+ description: 'Will be triggered when a specific user tweet something new.',
+ substeps: [
+ {
+ key: 'chooseConnection',
+ name: 'Choose connection',
+ },
+ {
+ key: 'chooseTrigger',
+ name: 'Set up a trigger',
+ arguments: [
+ {
+ label: 'Username',
+ key: 'username',
+ type: 'string',
+ required: true,
+ },
+ ],
+ },
+ {
+ key: 'testStep',
+ name: 'Test trigger',
+ },
+ ],
+
+ async run($: IGlobalVariable) {
+ return await getUserTweets($, {
+ currentUser: false,
+ userId: $.db.step.parameters.username as string,
+ lastInternalId: $.db.flow.lastInternalId,
+ });
+ },
+
+ async testRun($: IGlobalVariable) {
+ return await getUserTweets($, {
+ currentUser: false,
+ userId: $.db.step.parameters.username as string,
+ });
+ },
+};
diff --git a/packages/backend/src/graphql/mutations/create-auth-data.ts b/packages/backend/src/graphql/mutations/create-auth-data.ts
index 8a4b8886..91ff8d60 100644
--- a/packages/backend/src/graphql/mutations/create-auth-data.ts
+++ b/packages/backend/src/graphql/mutations/create-auth-data.ts
@@ -1,5 +1,7 @@
import Context from '../../types/express/context';
import axios from 'axios';
+import globalVariable from '../../helpers/global-variable';
+import App from '../../models/app';
type Params = {
input: {
@@ -19,29 +21,24 @@ const createAuthData = async (
})
.throwIfNotFound();
- const appClass = (await import(`../../apps/${connection.key}`)).default;
-
if (!connection.formattedData) {
return null;
}
- const appInstance = new appClass(connection);
- const authLink = await appInstance.authenticationClient.createAuthData();
+ const authInstance = (await import(`../../apps/${connection.key}/auth`))
+ .default;
+ const app = await App.findOneByKey(connection.key);
+
+ const $ = await globalVariable(connection, app);
+ await authInstance.createAuthData($);
try {
- await axios.get(authLink.url);
+ await axios.get(connection.formattedData.url as string);
} catch (error) {
throw new Error('Error occured while creating authorization URL!');
}
- await connection.$query().patch({
- formattedData: {
- ...connection.formattedData,
- ...authLink,
- },
- });
-
- return authLink;
+ return connection.formattedData;
};
export default createAuthData;
diff --git a/packages/backend/src/graphql/mutations/create-connection.ts b/packages/backend/src/graphql/mutations/create-connection.ts
index ac84cd74..fcaf093d 100644
--- a/packages/backend/src/graphql/mutations/create-connection.ts
+++ b/packages/backend/src/graphql/mutations/create-connection.ts
@@ -13,7 +13,7 @@ const createConnection = async (
params: Params,
context: Context
) => {
- App.findOneByKey(params.input.key);
+ await App.findOneByKey(params.input.key);
return await context.currentUser.$relatedQuery('connections').insert({
key: params.input.key,
diff --git a/packages/backend/src/graphql/mutations/update-flow-status.ts b/packages/backend/src/graphql/mutations/update-flow-status.ts
index 11ec676e..5e0e0297 100644
--- a/packages/backend/src/graphql/mutations/update-flow-status.ts
+++ b/packages/backend/src/graphql/mutations/update-flow-status.ts
@@ -33,7 +33,7 @@ const updateFlowStatus = async (
const triggerStep = await flow.getTriggerStep();
const trigger = await triggerStep.getTrigger();
- const interval = trigger.interval;
+ const interval = trigger?.getInterval(triggerStep.parameters);
const repeatOptions = {
cron: interval || EVERY_15_MINUTES_CRON,
};
diff --git a/packages/backend/src/graphql/mutations/update-step.ts b/packages/backend/src/graphql/mutations/update-step.ts
index 45fd1295..0c5adbed 100644
--- a/packages/backend/src/graphql/mutations/update-step.ts
+++ b/packages/backend/src/graphql/mutations/update-step.ts
@@ -1,3 +1,4 @@
+import { IJSONObject } from '@automatisch/types';
import Step from '../../models/step';
import Context from '../../types/express/context';
@@ -6,7 +7,7 @@ type Params = {
id: string;
key: string;
appKey: string;
- parameters: Record;
+ parameters: IJSONObject;
flow: {
id: string;
};
diff --git a/packages/backend/src/graphql/mutations/verify-connection.ts b/packages/backend/src/graphql/mutations/verify-connection.ts
index b0863aa5..081752b9 100644
--- a/packages/backend/src/graphql/mutations/verify-connection.ts
+++ b/packages/backend/src/graphql/mutations/verify-connection.ts
@@ -1,5 +1,6 @@
import Context from '../../types/express/context';
import App from '../../models/app';
+import globalVariable from '../../helpers/global-variable';
type Params = {
input: {
@@ -19,18 +20,11 @@ const verifyConnection = async (
})
.throwIfNotFound();
- const appClass = (await import(`../../apps/${connection.key}`)).default;
- const app = App.findOneByKey(connection.key);
-
- const appInstance = new appClass(connection);
- const verifiedCredentials =
- await appInstance.authenticationClient.verifyCredentials();
+ const app = await App.findOneByKey(connection.key);
+ const $ = await globalVariable(connection, app);
+ await app.auth.verifyCredentials($);
connection = await connection.$query().patchAndFetch({
- formattedData: {
- ...connection.formattedData,
- ...verifiedCredentials,
- },
verified: true,
draft: false,
});
diff --git a/packages/backend/src/graphql/queries/get-app.ts b/packages/backend/src/graphql/queries/get-app.ts
index 32479096..83f116d9 100644
--- a/packages/backend/src/graphql/queries/get-app.ts
+++ b/packages/backend/src/graphql/queries/get-app.ts
@@ -6,7 +6,7 @@ type Params = {
};
const getApp = async (_parent: unknown, params: Params, context: Context) => {
- const app = App.findOneByKey(params.key);
+ const app = await App.findOneByKey(params.key);
if (context.currentUser) {
const connections = await context.currentUser
diff --git a/packages/backend/src/graphql/queries/get-apps.ts b/packages/backend/src/graphql/queries/get-apps.ts
index 351fc394..d0928b44 100644
--- a/packages/backend/src/graphql/queries/get-apps.ts
+++ b/packages/backend/src/graphql/queries/get-apps.ts
@@ -6,8 +6,8 @@ type Params = {
onlyWithTriggers: boolean;
};
-const getApps = (_parent: unknown, params: Params) => {
- const apps = App.findAll(params.name);
+const getApps = async (_parent: unknown, params: Params) => {
+ const apps = await App.findAll(params.name);
if (params.onlyWithTriggers) {
return apps.filter((app: IApp) => app.triggers?.length);
diff --git a/packages/backend/src/graphql/queries/get-connected-apps.ts b/packages/backend/src/graphql/queries/get-connected-apps.ts
index ecf18847..a988e60c 100644
--- a/packages/backend/src/graphql/queries/get-connected-apps.ts
+++ b/packages/backend/src/graphql/queries/get-connected-apps.ts
@@ -11,7 +11,7 @@ const getConnectedApps = async (
params: Params,
context: Context
) => {
- let apps = App.findAll(params.name);
+ let apps = await App.findAll(params.name);
const connections = await context.currentUser
.$relatedQuery('connections')
diff --git a/packages/backend/src/graphql/queries/get-data.ts b/packages/backend/src/graphql/queries/get-data.ts
index f4fd4959..5b158869 100644
--- a/packages/backend/src/graphql/queries/get-data.ts
+++ b/packages/backend/src/graphql/queries/get-data.ts
@@ -1,5 +1,7 @@
-import { IJSONObject } from '@automatisch/types';
+import { IData, IJSONObject } from '@automatisch/types';
import Context from '../../types/express/context';
+import App from '../../models/app';
+import globalVariable from '../../helpers/global-variable';
type Params = {
stepId: string;
@@ -22,13 +24,18 @@ const getData = async (_parent: unknown, params: Params, context: Context) => {
if (!connection || !step.appKey) return null;
- const AppClass = (await import(`../../apps/${step.appKey}`)).default;
- const appInstance = new AppClass(connection, step.flow, step);
+ const app = await App.findOneByKey(step.appKey);
+ const $ = await globalVariable(connection, app, step.flow, step);
- const command = appInstance.data[params.key];
- const fetchedData = await command.run();
+ const command = app.data.find((data: IData) => data.key === params.key);
- return fetchedData;
+ const fetchedData = await command.run($);
+
+ if (fetchedData.error) {
+ throw new Error(JSON.stringify(fetchedData.error));
+ }
+
+ return fetchedData.data;
};
export default getData;
diff --git a/packages/backend/src/graphql/queries/test-connection.ts b/packages/backend/src/graphql/queries/test-connection.ts
index 1b9a01f5..5aa90fd1 100644
--- a/packages/backend/src/graphql/queries/test-connection.ts
+++ b/packages/backend/src/graphql/queries/test-connection.ts
@@ -1,4 +1,6 @@
import Context from '../../types/express/context';
+import App from '../../models/app';
+import globalVariable from '../../helpers/global-variable';
type Params = {
id: string;
@@ -17,11 +19,11 @@ const testConnection = async (
})
.throwIfNotFound();
- const appClass = (await import(`../../apps/${connection.key}`)).default;
- const appInstance = new appClass(connection);
+ const app = await App.findOneByKey(connection.key, false);
+ const $ = await globalVariable(connection, app);
const isStillVerified =
- await appInstance.authenticationClient.isStillVerified();
+ await app.auth.isStillVerified($);
connection = await connection.$query().patchAndFetch({
formattedData: connection.formattedData,
diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql
index a6f97dc9..689f660c 100644
--- a/packages/backend/src/graphql/schema.graphql
+++ b/packages/backend/src/graphql/schema.graphql
@@ -104,14 +104,18 @@ type App {
authDocUrl: String
primaryColor: String
supportsConnections: Boolean
- fields: [Field]
- authenticationSteps: [AuthenticationStep]
- reconnectionSteps: [ReconnectionStep]
+ auth: AppAuth
triggers: [Trigger]
actions: [Action]
connections: [Connection]
}
+type AppAuth {
+ fields: [Field]
+ authenticationSteps: [AuthenticationStep]
+ reconnectionSteps: [ReconnectionStep]
+}
+
enum ArgumentEnumType {
integer
string
diff --git a/packages/backend/src/helpers/app-assets-handler.ts b/packages/backend/src/helpers/app-assets-handler.ts
index dd85808b..e537b7c1 100644
--- a/packages/backend/src/helpers/app-assets-handler.ts
+++ b/packages/backend/src/helpers/app-assets-handler.ts
@@ -1,11 +1,9 @@
import express, { Application } from 'express';
-import App from '../models/app';
const appAssetsHandler = async (app: Application) => {
- const appNames = App.list;
-
- appNames.forEach(appName => {
- const svgPath = `${__dirname}/../apps/${appName}/assets/favicon.svg`;
+ app.use('/apps/:appKey/assets/favicon.svg', (req, res, next) => {
+ const { appKey } = req.params;
+ const svgPath = `${__dirname}/../apps/${appKey}/assets/favicon.svg`;
const staticFileHandlerOptions = {
/**
* Disabling fallthrough is important to respond with HTTP 404.
@@ -15,11 +13,8 @@ const appAssetsHandler = async (app: Application) => {
};
const staticFileHandler = express.static(svgPath, staticFileHandlerOptions);
- app.use(
- `/apps/${appName}/assets/favicon.svg`,
- staticFileHandler
- )
- })
-}
+ return staticFileHandler(req, res, next);
+ });
+};
export default appAssetsHandler;
diff --git a/packages/backend/src/helpers/app-info-converter.ts b/packages/backend/src/helpers/app-info-converter.ts
index df736006..8ea38c2a 100644
--- a/packages/backend/src/helpers/app-info-converter.ts
+++ b/packages/backend/src/helpers/app-info-converter.ts
@@ -1,12 +1,22 @@
import type { IApp } from '@automatisch/types';
import appConfig from '../config/app';
-const appInfoConverter = (rawAppData: string) => {
- let computedRawData = rawAppData.replace('{BASE_URL}', appConfig.baseUrl);
- computedRawData = computedRawData.replace('{WEB_APP_URL}', appConfig.webAppUrl);
+const appInfoConverter = (rawAppData: IApp) => {
+ rawAppData.iconUrl = rawAppData.iconUrl.replace(
+ '{BASE_URL}',
+ appConfig.baseUrl
+ );
- const computedJSONData: IApp = JSON.parse(computedRawData)
- return computedJSONData;
-}
+ if (rawAppData.auth?.fields) {
+ rawAppData.auth.fields = rawAppData.auth.fields.map((field) => {
+ return {
+ ...field,
+ value: field.value?.replace('{WEB_APP_URL}', appConfig.webAppUrl),
+ };
+ });
+ }
+
+ return rawAppData;
+};
export default appInfoConverter;
diff --git a/packages/backend/src/helpers/get-app.ts b/packages/backend/src/helpers/get-app.ts
new file mode 100644
index 00000000..75913c45
--- /dev/null
+++ b/packages/backend/src/helpers/get-app.ts
@@ -0,0 +1,78 @@
+import fs from 'fs';
+import { join } from 'path';
+import { IApp, IAuth, IAction, ITrigger, IData } from '@automatisch/types';
+
+const appsPath = join(__dirname, '../apps');
+
+async function getDefaultExport(path: string) {
+ return (await import(path)).default;
+}
+
+function stripFunctions(data: C): C {
+ return JSON.parse(JSON.stringify(data));
+}
+
+async function getFileContent(
+ path: string,
+ stripFuncs: boolean
+): Promise {
+ try {
+ const fileContent = await getDefaultExport(path);
+
+ if (stripFuncs) {
+ return stripFunctions(fileContent);
+ }
+
+ return fileContent;
+ } catch (err) {
+ return null;
+ }
+}
+
+async function getChildrenContentInDirectory(
+ path: string,
+ stripFuncs: boolean
+): Promise {
+ const appSubdirectory = join(appsPath, path);
+ const childrenContent = [];
+
+ if (fs.existsSync(appSubdirectory)) {
+ const filesInSubdirectory = fs.readdirSync(appSubdirectory);
+
+ for (const filename of filesInSubdirectory) {
+ const filePath = join(appSubdirectory, filename, 'index.ts');
+ const fileContent = await getFileContent(filePath, stripFuncs);
+
+ childrenContent.push(fileContent);
+ }
+
+ return childrenContent;
+ }
+
+ return [];
+}
+
+const getApp = async (appKey: string, stripFuncs = true) => {
+ const appData: IApp = await getDefaultExport(`../apps/${appKey}`);
+
+ appData.auth = await getFileContent(
+ `../apps/${appKey}/auth/index.ts`,
+ stripFuncs
+ );
+ appData.triggers = await getChildrenContentInDirectory(
+ `${appKey}/triggers`,
+ stripFuncs
+ );
+ appData.actions = await getChildrenContentInDirectory(
+ `${appKey}/actions`,
+ stripFuncs
+ );
+ appData.data = await getChildrenContentInDirectory(
+ `${appKey}/data`,
+ stripFuncs
+ );
+
+ return appData;
+};
+
+export default getApp;
diff --git a/packages/backend/src/helpers/global-variable.ts b/packages/backend/src/helpers/global-variable.ts
new file mode 100644
index 00000000..e064a8cb
--- /dev/null
+++ b/packages/backend/src/helpers/global-variable.ts
@@ -0,0 +1,42 @@
+import createHttpClient from './http-client';
+import Connection from '../models/connection';
+import Flow from '../models/flow';
+import Step from '../models/step';
+import { IJSONObject, IApp, IGlobalVariable } from '@automatisch/types';
+
+const globalVariable = async (
+ connection: Connection,
+ appData: IApp,
+ flow?: Flow,
+ currentStep?: Step
+): Promise => {
+ const lastInternalId = await flow?.lastInternalId();
+
+ return {
+ auth: {
+ set: async (args: IJSONObject) => {
+ await connection.$query().patchAndFetch({
+ formattedData: {
+ ...connection.formattedData,
+ ...args,
+ },
+ });
+
+ return null;
+ },
+ data: connection.formattedData,
+ },
+ app: appData,
+ http: createHttpClient({ baseURL: appData.baseUrl }),
+ db: {
+ flow: {
+ lastInternalId,
+ },
+ step: {
+ parameters: currentStep?.parameters || {},
+ },
+ },
+ };
+};
+
+export default globalVariable;
diff --git a/packages/backend/src/models/app.ts b/packages/backend/src/models/app.ts
index 62151633..2a52c810 100644
--- a/packages/backend/src/models/app.ts
+++ b/packages/backend/src/models/app.ts
@@ -2,6 +2,7 @@ import fs from 'fs';
import { join } from 'path';
import { IApp } from '@automatisch/types';
import appInfoConverter from '../helpers/app-info-converter';
+import getApp from '../helpers/get-app';
class App {
static folderPath = join(__dirname, '../apps');
@@ -11,28 +12,30 @@ class App {
// their actions/triggers are implemented!
static temporaryList = ['slack', 'twitter', 'scheduler'];
- static findAll(name?: string): IApp[] {
+ static async findAll(name?: string, stripFuncs = true): Promise {
if (!name)
- return this.temporaryList.map((name) => this.findOneByName(name));
+ return Promise.all(
+ this.temporaryList.map(
+ async (name) => await this.findOneByName(name, stripFuncs)
+ )
+ );
- return this.temporaryList
- .filter((app) => app.includes(name.toLowerCase()))
- .map((name) => this.findOneByName(name));
+ return Promise.all(
+ this.temporaryList
+ .filter((app) => app.includes(name.toLowerCase()))
+ .map((name) => this.findOneByName(name, stripFuncs))
+ );
}
- static findOneByName(name: string): IApp {
- const rawAppData = fs.readFileSync(
- this.folderPath + `/${name}/info.json`,
- 'utf-8'
- );
+ static async findOneByName(name: string, stripFuncs = false): Promise {
+ const rawAppData = await getApp(name.toLocaleLowerCase(), stripFuncs);
+
return appInfoConverter(rawAppData);
}
- static findOneByKey(key: string): IApp {
- const rawAppData = fs.readFileSync(
- this.folderPath + `/${key}/info.json`,
- 'utf-8'
- );
+ static async findOneByKey(key: string, stripFuncs = false): Promise {
+ const rawAppData = await getApp(key, stripFuncs);
+
return appInfoConverter(rawAppData);
}
}
diff --git a/packages/backend/src/models/connection.ts b/packages/backend/src/models/connection.ts
index 8d44fd07..34968a19 100644
--- a/packages/backend/src/models/connection.ts
+++ b/packages/backend/src/models/connection.ts
@@ -12,7 +12,7 @@ import Telemetry from '../helpers/telemetry';
class Connection extends Base {
id!: string;
key!: string;
- data = '';
+ data: string;
formattedData?: IJSONObject;
userId!: string;
verified = false;
@@ -56,10 +56,6 @@ class Connection extends Base {
},
});
- get appData() {
- return App.findOneByKey(this.key);
- }
-
encryptData(): void {
if (!this.eligibleForEncryption()) return;
diff --git a/packages/backend/src/models/execution-step.ts b/packages/backend/src/models/execution-step.ts
index 2f4c45ee..2d01e7fb 100644
--- a/packages/backend/src/models/execution-step.ts
+++ b/packages/backend/src/models/execution-step.ts
@@ -3,14 +3,15 @@ import Base from './base';
import Execution from './execution';
import Step from './step';
import Telemetry from '../helpers/telemetry';
+import { IJSONObject } from '@automatisch/types';
class ExecutionStep extends Base {
id!: string;
executionId!: string;
stepId!: string;
- dataIn!: Record;
- dataOut!: Record;
- errorDetails: Record;
+ dataIn!: IJSONObject;
+ dataOut!: IJSONObject;
+ errorDetails: IJSONObject;
status = 'failure';
step: Step;
@@ -23,7 +24,7 @@ class ExecutionStep extends Base {
id: { type: 'string', format: 'uuid' },
executionId: { type: 'string', format: 'uuid' },
stepId: { type: 'string' },
- dataIn: { type: 'object' },
+ dataIn: { type: ['object', 'null'] },
dataOut: { type: ['object', 'null'] },
status: { type: 'string', enum: ['success', 'failure'] },
errorDetails: { type: ['object', 'null'] },
diff --git a/packages/backend/src/models/flow.ts b/packages/backend/src/models/flow.ts
index 388c477c..db536431 100644
--- a/packages/backend/src/models/flow.ts
+++ b/packages/backend/src/models/flow.ts
@@ -10,7 +10,7 @@ class Flow extends Base {
name!: string;
userId!: string;
active: boolean;
- steps?: [Step];
+ steps: Step[];
published_at: string;
static tableName = 'flows';
diff --git a/packages/backend/src/models/step.ts b/packages/backend/src/models/step.ts
index 61579f40..b69f3d20 100644
--- a/packages/backend/src/models/step.ts
+++ b/packages/backend/src/models/step.ts
@@ -4,7 +4,7 @@ import App from './app';
import Flow from './flow';
import Connection from './connection';
import ExecutionStep from './execution-step';
-import type { IStep } from '@automatisch/types';
+import type { IJSONObject, IStep } from '@automatisch/types';
import Telemetry from '../helpers/telemetry';
import appConfig from '../config/app';
@@ -17,10 +17,10 @@ class Step extends Base {
connectionId?: string;
status = 'incomplete';
position!: number;
- parameters: Record;
+ parameters: IJSONObject;
connection?: Connection;
flow: Flow;
- executionSteps?: [ExecutionStep];
+ executionSteps: ExecutionStep[];
static tableName = 'steps';
@@ -78,10 +78,6 @@ class Step extends Base {
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`;
}
- get appData() {
- return App.findOneByKey(this.appKey);
- }
-
async $afterInsert(queryContext: QueryContext) {
await super.$afterInsert(queryContext);
Telemetry.stepCreated(this);
@@ -101,12 +97,8 @@ class Step extends Base {
const { appKey, key } = this;
- const connection = await this.$relatedQuery('connection');
- const flow = await this.$relatedQuery('flow');
-
- const AppClass = (await import(`../apps/${appKey}`)).default;
- const appInstance = new AppClass(connection, flow, this);
- const command = appInstance.triggers[key];
+ const app = await App.findOneByKey(appKey);
+ const command = app.triggers.find((trigger) => trigger.key === key);
return command;
}
diff --git a/packages/backend/src/models/user.ts b/packages/backend/src/models/user.ts
index f753b889..35600ba1 100644
--- a/packages/backend/src/models/user.ts
+++ b/packages/backend/src/models/user.ts
@@ -10,9 +10,9 @@ class User extends Base {
id!: string;
email!: string;
password!: string;
- connections?: [Connection];
- flows?: [Flow];
- steps?: [Step];
+ connections?: Connection[];
+ flows?: Flow[];
+ steps?: Step[];
static tableName = 'users';
diff --git a/packages/backend/src/services/processor.ts b/packages/backend/src/services/processor.ts
index 21d58db6..224fcceb 100644
--- a/packages/backend/src/services/processor.ts
+++ b/packages/backend/src/services/processor.ts
@@ -1,9 +1,12 @@
import get from 'lodash.get';
+import { IJSONObject } from '@automatisch/types';
+
+import App from '../models/app';
import Flow from '../models/flow';
import Step from '../models/step';
import Execution from '../models/execution';
import ExecutionStep from '../models/execution-step';
-import { IJSONObject } from '@automatisch/types';
+import globalVariable from '../helpers/global-variable';
type ExecutionSteps = Record;
@@ -70,7 +73,7 @@ class Processor {
const execution = await Execution.query().insert({
flowId: this.flow.id,
testRun: this.testRun,
- internalId: data.id,
+ internalId: data.id as string,
});
executions.push(execution);
@@ -92,7 +95,7 @@ class Processor {
const { appKey, key, type, parameters: rawParameters = {}, id } = step;
const isTrigger = type === 'trigger';
- const AppClass = (await import(`../apps/${appKey}`)).default;
+ const app = await App.findOneByKey(appKey);
const computedParameters = Processor.computeParameters(
rawParameters,
@@ -101,19 +104,24 @@ class Processor {
step.parameters = computedParameters;
- const appInstance = new AppClass(step.connection, this.flow, step);
+ const $ = await globalVariable(
+ step.connection,
+ app,
+ this.flow,
+ step,
+ );
if (!isTrigger && key) {
- const command = appInstance.actions[key];
- fetchedActionData = await command.run();
+ const command = app.actions.find((action) => action.key === key);
+ fetchedActionData = await command.run($);
}
if (!isTrigger && fetchedActionData.error) {
await execution.$relatedQuery('executionSteps').insertAndFetch({
stepId: id,
status: 'failure',
- dataIn: computedParameters,
- dataOut: null,
+ dataIn: null,
+ dataOut: computedParameters,
errorDetails: fetchedActionData.error,
});
@@ -166,19 +174,22 @@ class Processor {
async getInitialTriggerData(step: Step) {
if (!step.appKey || !step.key) return null;
- const AppClass = (await import(`../apps/${step.appKey}`)).default;
- const appInstance = new AppClass(step.connection, this.flow, step);
+ const app = await App.findOneByKey(step.appKey);
+ const $ = await globalVariable(
+ step.connection,
+ app,
+ this.flow,
+ step,
+ )
- const command = appInstance.triggers[step.key];
+ const command = app.triggers.find((trigger) => trigger.key === step.key);
let fetchedData;
- const lastInternalId = await this.flow.lastInternalId();
-
if (this.testRun) {
- fetchedData = await command.testRun();
+ fetchedData = await command.testRun($);
} else {
- fetchedData = await command.run(lastInternalId);
+ fetchedData = await command.run($);
}
return fetchedData;
diff --git a/packages/backend/src/types/axios.d.ts b/packages/backend/src/types/axios.d.ts
deleted file mode 100644
index 948f46b4..00000000
--- a/packages/backend/src/types/axios.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { IJSONObject } from '@automatisch/types';
-
-declare module 'axios' {
- interface AxiosResponse {
- integrationError?: IJSONObject;
- }
-}
diff --git a/packages/backend/tsconfig.json b/packages/backend/tsconfig.json
index 3888c0b7..0f5693bd 100644
--- a/packages/backend/tsconfig.json
+++ b/packages/backend/tsconfig.json
@@ -9,23 +9,26 @@
"noImplicitAny": true,
"outDir": "dist",
"paths": {
- "*": [
- "../../node_modules/*",
- "node_modules/*",
- "src/types/*"
- ]
+ "*": ["../../node_modules/*", "node_modules/*", "src/types/*"]
},
"skipLibCheck": true,
"sourceMap": true,
"target": "es2021",
"typeRoots": [
"node_modules/@types",
+ "node_modules/@automatisch/types",
"./src/types",
"./src/apps"
]
},
- "include": [
- "src/**/*",
- "bin/**/*"
+ "include": ["src/**/*", "bin/**/*"],
+ "exclude": [
+ "src/apps/discord",
+ "src/apps/firebase",
+ "src/apps/flickr",
+ "src/apps/github",
+ "src/apps/gitlab",
+ "src/apps/twitch",
+ "src/apps/typeform"
]
}
diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts
index 49ec37c4..43898860 100644
--- a/packages/types/index.d.ts
+++ b/packages/types/index.d.ts
@@ -1,3 +1,6 @@
+import type { AxiosInstance } from 'axios';
+export type IHttpClient = AxiosInstance;
+
// Type definitions for automatisch
export type IJSONValue = string | number | boolean | IJSONObject | IJSONArray;
@@ -10,11 +13,11 @@ export interface IConnection {
id: string;
key: string;
data: string;
- formattedData: IJSONObject;
+ formattedData?: IJSONObject;
userId: string;
verified: boolean;
- count: number;
- flowCount: number;
+ count?: number;
+ flowCount?: number;
appData?: IApp;
createdAt: string;
}
@@ -22,7 +25,7 @@ export interface IConnection {
export interface IExecutionStep {
id: string;
executionId: string;
- stepId: IStep["id"];
+ stepId: IStep['id'];
step: IStep;
dataIn: IJSONObject;
dataOut: IJSONObject;
@@ -44,21 +47,21 @@ export interface IExecution {
export interface IStep {
id: string;
- name: string;
+ name?: string;
flowId: string;
- key: string;
- appKey: string;
+ key?: string;
+ appKey?: string;
iconUrl: string;
type: 'action' | 'trigger';
- connectionId: string;
+ connectionId?: string;
status: string;
position: number;
- parameters: Record;
- connection: Partial;
+ parameters: IJSONObject;
+ connection?: Partial;
flow: IFlow;
executionSteps: IExecutionStep[];
// FIXME: remove this property once execution steps are properly exposed via queries
- output: IJSONObject;
+ output?: IJSONObject;
appData?: IApp;
}
@@ -128,7 +131,7 @@ export interface IFieldText {
dependsOn: string[];
}
-type IField = IFieldDropdown | IFieldText;
+export type IField = IFieldDropdown | IFieldText;
export interface IAuthenticationStepField {
name: string;
@@ -154,14 +157,27 @@ export interface IApp {
authDocUrl: string;
primaryColor: string;
supportsConnections: boolean;
+ baseUrl: string;
+ auth: IAuth;
+ connectionCount: number;
+ flowCount: number;
+ data: IData;
+ triggers: ITrigger[];
+ actions: IAction[];
+ connections: IConnection[];
+}
+
+export interface IData {
+ [index: string]: any;
+}
+
+export interface IAuth {
+ createAuthData($: IGlobalVariable): Promise,
+ verifyCredentials($: IGlobalVariable): Promise,
+ isStillVerified($: IGlobalVariable): Promise,
fields: IField[];
authenticationSteps: IAuthenticationStep[];
reconnectionSteps: IAuthenticationStep[];
- connectionCount: number;
- flowCount: number;
- triggers: any[];
- actions: any[];
- connections: IConnection[];
}
export interface IService {
@@ -172,8 +188,22 @@ export interface IService {
}
export interface ITrigger {
- run(startTime?: Date): Promise;
- testRun(startTime?: Date): Promise;
+ name: string;
+ key: string,
+ pollInterval: number;
+ description: string;
+ substeps: ISubstep[];
+ getInterval(parameters: IGlobalVariable["db"]["step"]["parameters"]): string;
+ run($: IGlobalVariable): Promise<{ data: IJSONObject[], error: IJSONObject | null }>;
+ testRun($: IGlobalVariable, startTime?: Date): Promise<{ data: IJSONObject[], error: IJSONObject | null }>;
+}
+
+export interface IAction {
+ name: string;
+ key: string,
+ description: string;
+ substeps: ISubstep[];
+ run($: IGlobalVariable): Promise<{ data: IJSONObject, error: IJSONObject | null }>;
}
export interface IAuthentication {
@@ -183,10 +213,34 @@ export interface IAuthentication {
}
export interface ISubstep {
+ key: string;
name: string;
arguments: IField[];
}
export type IHttpClientParams = {
baseURL?: string;
+};
+
+export type IGlobalVariable = {
+ auth: {
+ set: (args: IJSONObject) => Promise;
+ data: IJSONObject;
+ };
+ app: IApp;
+ http: IHttpClient;
+ db: {
+ flow: {
+ lastInternalId: string;
+ };
+ step: {
+ parameters: IJSONObject;
+ }
+ };
+};
+
+declare module 'axios' {
+ interface AxiosResponse {
+ integrationError?: IJSONObject;
+ }
}
diff --git a/packages/web/src/components/AddAppConnection/index.tsx b/packages/web/src/components/AddAppConnection/index.tsx
index ddbf5359..3759514b 100644
--- a/packages/web/src/components/AddAppConnection/index.tsx
+++ b/packages/web/src/components/AddAppConnection/index.tsx
@@ -30,7 +30,8 @@ type Response = {
export default function AddAppConnection(props: AddAppConnectionProps): React.ReactElement {
const { application, connectionId, onClose } = props;
- const { name, authDocUrl, key, fields, authenticationSteps, reconnectionSteps } = application;
+ const { name, authDocUrl, key, auth } = application;
+ const { fields, authenticationSteps, reconnectionSteps } = auth;
const formatMessage = useFormatMessage();
const [errorMessage, setErrorMessage] = React.useState(null);
const [inProgress, setInProgress] = React.useState(false);
diff --git a/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx b/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx
index e54d1d3b..396d4b78 100644
--- a/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx
+++ b/packages/web/src/components/ChooseAppAndEventSubstep/index.tsx
@@ -12,10 +12,10 @@ import useFormatMessage from 'hooks/useFormatMessage';
import { EditorContext } from 'contexts/Editor';
import { GET_APPS } from 'graphql/queries/get-apps';
import FlowSubstepTitle from 'components/FlowSubstepTitle';
-import type { IApp, IStep, ISubstep } from '@automatisch/types';
+import type { IApp, IStep, ISubstep, ITrigger, IAction } from '@automatisch/types';
type ChooseAppAndEventSubstepProps = {
- substep: ISubstep,
+ substep: ISubstep;
expanded?: boolean;
onExpand: () => void;
onCollapse: () => void;
@@ -24,14 +24,17 @@ type ChooseAppAndEventSubstepProps = {
step: IStep;
};
-const optionGenerator = (app: IApp): { label: string; value: string; } => ({
+const optionGenerator = (app: { name: string, key: string, }): { label: string; value: string } => ({
label: app.name as string,
value: app.key as string,
});
-const getOption = (options: Record[], appKey: IApp["key"]) => options.find(option => option.value === appKey as string) || null;
+const getOption = (options: Record[], appKey?: IApp['key']) =>
+ options.find((option) => option.value === (appKey as string)) || null;
-function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.ReactElement {
+function ChooseAppAndEventSubstep(
+ props: ChooseAppAndEventSubstepProps
+): React.ReactElement {
const {
substep,
expanded = false,
@@ -47,59 +50,74 @@ function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.R
const isTrigger = step.type === 'trigger';
- const { data } = useQuery(GET_APPS, { variables: { onlyWithTriggers: isTrigger }});
+ const { data } = useQuery(GET_APPS, {
+ variables: { onlyWithTriggers: isTrigger },
+ });
const apps: IApp[] = data?.getApps;
const app = apps?.find((currentApp: IApp) => currentApp.key === step.appKey);
- const appOptions = React.useMemo(() => apps?.map((app) => optionGenerator(app)), [apps]);
- const actionsOrTriggers = isTrigger ? app?.triggers : app?.actions;
- const actionOptions = React.useMemo(() => actionsOrTriggers?.map((trigger) => optionGenerator(trigger)) ?? [], [app?.key]);
- const selectedActionOrTrigger = actionsOrTriggers?.find((actionOrTrigger) => actionOrTrigger.key === step?.key) || null;
+ const appOptions = React.useMemo(
+ () => apps?.map((app) => optionGenerator(app)),
+ [apps]
+ );
+ const actionsOrTriggers: Array = (isTrigger ? app?.triggers : app?.actions) || [];
+ const actionOptions = React.useMemo(
+ () => actionsOrTriggers.map((trigger) => optionGenerator(trigger)),
+ [app?.key]
+ );
+ const selectedActionOrTrigger =
+ actionsOrTriggers.find(
+ (actionOrTrigger: IAction | ITrigger) => actionOrTrigger.key === step?.key
+ );
- const {
- name,
- } = substep;
+ const { name } = substep;
const valid: boolean = !!step.key && !!step.appKey;
// placeholders
- const onEventChange = React.useCallback((event: React.SyntheticEvent, selectedOption: unknown) => {
- if (typeof selectedOption === 'object') {
- // TODO: try to simplify type casting below.
- const typedSelectedOption = selectedOption as { value: string };
- const option: { value: string } = typedSelectedOption;
- const eventKey = option?.value as string;
+ const onEventChange = React.useCallback(
+ (event: React.SyntheticEvent, selectedOption: unknown) => {
+ if (typeof selectedOption === 'object') {
+ // TODO: try to simplify type casting below.
+ const typedSelectedOption = selectedOption as { value: string };
+ const option: { value: string } = typedSelectedOption;
+ const eventKey = option?.value as string;
- if (step.key !== eventKey) {
- onChange({
- step: {
- ...step,
- key: eventKey,
- },
- });
+ if (step.key !== eventKey) {
+ onChange({
+ step: {
+ ...step,
+ key: eventKey,
+ },
+ });
+ }
}
- }
- }, [step, onChange]);
+ },
+ [step, onChange]
+ );
- const onAppChange = React.useCallback((event: React.SyntheticEvent, selectedOption: unknown) => {
- if (typeof selectedOption === 'object') {
- // TODO: try to simplify type casting below.
- const typedSelectedOption = selectedOption as { value: string };
- const option: { value: string } = typedSelectedOption;
- const appKey = option?.value as string;
+ const onAppChange = React.useCallback(
+ (event: React.SyntheticEvent, selectedOption: unknown) => {
+ if (typeof selectedOption === 'object') {
+ // TODO: try to simplify type casting below.
+ const typedSelectedOption = selectedOption as { value: string };
+ const option: { value: string } = typedSelectedOption;
+ const appKey = option?.value as string;
- if (step.appKey !== appKey) {
- onChange({
- step: {
- ...step,
- key: '',
- appKey,
- parameters: {},
- },
- });
+ if (step.appKey !== appKey) {
+ onChange({
+ step: {
+ ...step,
+ key: '',
+ appKey,
+ parameters: {},
+ },
+ });
+ }
}
- }
- }, [step, onChange]);
+ },
+ [step, onChange]
+ );
const onToggle = expanded ? onCollapse : onExpand;
@@ -112,14 +130,26 @@ function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.R
valid={valid}
/>
-
+
}
+ renderInput={(params) => (
+
+ )}
value={getOption(appOptions, step.appKey)}
onChange={onAppChange}
/>
@@ -137,17 +167,24 @@ function ChooseAppAndEventSubstep(props: ChooseAppAndEventSubstepProps): React.R
disableClearable
disabled={editorContext.readOnly}
options={actionOptions}
- renderInput={(params) => }
+ renderInput={(params) => (
+
+ )}
value={getOption(actionOptions, step.key)}
onChange={onEventChange}
/>
)}
- {isTrigger && selectedActionOrTrigger?.pollInterval && (
+ {isTrigger && (selectedActionOrTrigger as ITrigger)?.pollInterval && (
currentApp.key === step.appKey);
- const actionsOrTriggers = isTrigger ? app?.triggers : app?.actions;
+ const actionsOrTriggers: Array = (isTrigger ? app?.triggers : app?.actions) || [];
const substeps = React.useMemo(
() =>
- actionsOrTriggers?.find(({ key }) => key === step.key)?.substeps || [],
+ actionsOrTriggers?.find(({ key }: ITrigger | IAction) => key === step.key)?.substeps || [],
[actionsOrTriggers, step?.key]
);
@@ -235,7 +235,7 @@ export default function FlowStep(
>
toggleSubstep(0)}
onCollapse={() => toggleSubstep(0)}
onSubmit={expandNextStep}
@@ -246,11 +246,7 @@ export default function FlowStep(
{substeps?.length > 0 &&
substeps.map(
(
- substep: {
- name: string;
- key: string;
- arguments: IField[];
- },
+ substep: ISubstep,
index: number
) => (
@@ -279,7 +275,7 @@ export default function FlowStep(
/>
)}
- {['chooseConnection', 'testStep'].includes(substep.key) ===
+ {substep.key && ['chooseConnection', 'testStep'].includes(substep.key) ===
false && (
{
return array.slice(0, length);
-}
+};
const Suggestions = (props: SuggestionsProps) => {
- const {
- data,
- onSuggestionClick = () => null,
- } = props;
+ const { data, onSuggestionClick = () => null } = props;
const [current, setCurrent] = React.useState(0);
const [listLength, setListLength] = React.useState(SHORT_LIST_LENGTH);
@@ -40,41 +37,43 @@ const Suggestions = (props: SuggestionsProps) => {
const collapseList = () => {
setListLength(SHORT_LIST_LENGTH);
- }
+ };
React.useEffect(() => {
setListLength(SHORT_LIST_LENGTH);
- }, [current])
+ }, [current]);
return (
-
- Variables
-
+
+
+ Variables
+
+
{data.map((option: IStep, index: number) => (
<>
setCurrent((currentIndex) => currentIndex === index ? null : index)}
- sx={{ py: 0.5, }}
+ onClick={() =>
+ setCurrent((currentIndex) =>
+ currentIndex === index ? null : index
+ )
+ }
+ sx={{ py: 0.5 }}
>
-
+
- {!!option.output?.length && (
- current === index ? :
- )}
+ {!!option.output?.length &&
+ (current === index ? : )}
-
- {getPartialArray(option.output as any || [], listLength)
- .map((suboption: any, index: number) => (
+
+ {getPartialArray((option.output as any) || [], listLength).map(
+ (suboption: any, index: number) => (
{
primaryTypographyProps={{
variant: 'subtitle1',
title: 'Property name',
- sx: { fontWeight: 700 }
+ sx: { fontWeight: 700 },
}}
secondary={suboption.value || ''}
secondaryTypographyProps={{
@@ -95,24 +94,18 @@ const Suggestions = (props: SuggestionsProps) => {
}}
/>
- ))
- }
+ )
+ )}
- {option.output?.length > listLength && (
-
);
-}
+};
export default Suggestions;
diff --git a/packages/web/src/components/PowerInput/data.ts b/packages/web/src/components/PowerInput/data.ts
index 02198916..a62606e3 100644
--- a/packages/web/src/components/PowerInput/data.ts
+++ b/packages/web/src/components/PowerInput/data.ts
@@ -1,6 +1,7 @@
import type { IStep } from '@automatisch/types';
-const joinBy = (delimiter = '.', ...args: string[]) => args.filter(Boolean).join(delimiter);
+const joinBy = (delimiter = '.', ...args: string[]) =>
+ args.filter(Boolean).join(delimiter);
const process = (data: any, parentKey?: any, index?: number): any[] => {
if (typeof data !== 'object') {
@@ -8,14 +9,19 @@ const process = (data: any, parentKey?: any, index?: number): any[] => {
{
name: `${parentKey}.${index}`,
value: data,
- }
- ]
+ },
+ ];
}
const entries = Object.entries(data);
return entries.flatMap(([name, value]) => {
- const fullName = joinBy('.', parentKey, (index as number)?.toString(), name);
+ const fullName = joinBy(
+ '.',
+ parentKey,
+ (index as number)?.toString(),
+ name
+ );
if (Array.isArray(value)) {
return value.flatMap((item, index) => process(item, fullName, index));
@@ -25,10 +31,12 @@ const process = (data: any, parentKey?: any, index?: number): any[] => {
return process(value, fullName);
}
- return [{
- name: fullName,
- value,
- }];
+ return [
+ {
+ name: fullName,
+ value,
+ },
+ ];
});
};
@@ -39,12 +47,17 @@ export const processStepWithExecutions = (steps: IStep[]): any[] => {
.filter((step: IStep) => {
const hasExecutionSteps = !!step.executionSteps?.length;
- return hasExecutionSteps
+ return hasExecutionSteps;
})
.map((step: IStep, index: number) => ({
id: step.id,
// TODO: replace with step.name once introduced
- name: `${index + 1}. ${step.appKey?.charAt(0)?.toUpperCase() + step.appKey?.slice(1)}`,
- output: process(step.executionSteps?.[0]?.dataOut || {}, `step.${step.id}`),
+ name: `${index + 1}. ${
+ (step.appKey || '').charAt(0)?.toUpperCase() + step.appKey?.slice(1)
+ }`,
+ output: process(
+ step.executionSteps?.[0]?.dataOut || {},
+ `step.${step.id}`
+ ),
}));
};
diff --git a/packages/web/src/graphql/queries/get-app.ts b/packages/web/src/graphql/queries/get-app.ts
index 5c5aff67..ce3219ed 100644
--- a/packages/web/src/graphql/queries/get-app.ts
+++ b/packages/web/src/graphql/queries/get-app.ts
@@ -10,42 +10,44 @@ export const GET_APP = gql`
authDocUrl
primaryColor
supportsConnections
- fields {
- key
- label
- type
- required
- readOnly
- value
- description
- docUrl
- clickToCopy
- }
- authenticationSteps {
- step
- type
- name
- arguments {
- name
- value
+ auth {
+ fields {
+ key
+ label
type
- properties {
+ required
+ readOnly
+ value
+ description
+ docUrl
+ clickToCopy
+ }
+ authenticationSteps {
+ step
+ type
+ name
+ arguments {
name
value
+ type
+ properties {
+ name
+ value
+ }
}
}
- }
- reconnectionSteps {
- step
- type
- name
- arguments {
- name
- value
+ reconnectionSteps {
+ step
type
- properties {
+ name
+ arguments {
name
value
+ type
+ properties {
+ name
+ value
+ }
}
}
}
diff --git a/packages/web/src/graphql/queries/get-apps.ts b/packages/web/src/graphql/queries/get-apps.ts
index 3cdd6e3c..f22a3adf 100644
--- a/packages/web/src/graphql/queries/get-apps.ts
+++ b/packages/web/src/graphql/queries/get-apps.ts
@@ -10,43 +10,45 @@ export const GET_APPS = gql`
primaryColor
connectionCount
supportsConnections
- fields {
- key
- label
- type
- required
- readOnly
- value
- placeholder
- description
- docUrl
- clickToCopy
- }
- authenticationSteps {
- step
- type
- name
- arguments {
- name
- value
+ auth {
+ fields {
+ key
+ label
type
- properties {
+ required
+ readOnly
+ value
+ placeholder
+ description
+ docUrl
+ clickToCopy
+ }
+ authenticationSteps {
+ step
+ type
+ name
+ arguments {
name
value
+ type
+ properties {
+ name
+ value
+ }
}
}
- }
- reconnectionSteps {
- step
- type
- name
- arguments {
- name
- value
+ reconnectionSteps {
+ step
type
- properties {
+ name
+ arguments {
name
value
+ type
+ properties {
+ name
+ value
+ }
}
}
}