Merge branch 'main' into issue-553

This commit is contained in:
Ali BARIN
2022-10-08 15:03:51 +02:00
committed by GitHub
122 changed files with 14036 additions and 3174 deletions

View File

@@ -2,10 +2,4 @@
FROM node:16
WORKDIR /automatisch
# npm registry for dev purposes
RUN npm config set fetch-retry-maxtimeout 5000
RUN npm config set fetch-retry-mintimeout 3000
RUN npm set registry http://localhost:5000
# npm registry for dev purposes
RUN yarn global add @automatisch/cli
RUN yarn global add @automatisch/cli@0.1.4

View File

@@ -5,17 +5,11 @@ WORKDIR /automatisch
RUN apt-get update && apt-get install -y postgresql-client
COPY ./wait-for-postgres.sh /automatisch/wait-for-postgres.sh
# npm registry for dev purposes
RUN npm config set fetch-retry-maxtimeout 5000
RUN npm config set fetch-retry-mintimeout 3000
RUN npm set registry http://localhost:5000
# npm registry for dev purposes
RUN mkdir -p /automatisch/storage
RUN touch /automatisch/storage/.env
RUN echo "ENCRYPTION_KEY=$(openssl rand -base64 36)" >> /automatisch/storage/.env
RUN echo "APP_SECRET_KEY=$(openssl rand -base64 36)" >> /automatisch/storage/.env
RUN yarn global add @automatisch/cli
RUN yarn global add @automatisch/cli@0.1.4
EXPOSE 3000
CMD sh /automatisch/wait-for-postgres.sh automatisch start --env-file=/automatisch/storage/.env

View File

@@ -2,16 +2,12 @@
"packages": [
"packages/*"
],
"private": true,
"version": "independent",
"version": "0.1.4",
"npmClient": "yarn",
"useWorkspaces": true,
"command": {
"publish": {
"registry": "http://localhost:5000"
},
"add": {
"exact": true
}
}
}
}

View File

@@ -1,5 +1,6 @@
{
"name": "@automatisch/root",
"license": "AGPL-3.0",
"private": true,
"scripts": {
"start": "lerna run --stream --parallel --scope=@*/{web,backend} dev",
@@ -31,6 +32,6 @@
"prettier": "^2.5.1"
},
"publishConfig": {
"registry": "http://localhost:5000"
"access": "public"
}
}
}

View File

@@ -1,11 +1,4 @@
# `backend`
> TODO: description
## Usage
```
const backend = require('backend');
// TODO: DEMONSTRATE API
```
The open source Zapier alternative. Build workflow automation without spending
time and money.

View File

@@ -1,7 +1,8 @@
{
"name": "@automatisch/backend",
"version": "0.1.0",
"description": "> TODO: description",
"version": "0.1.4",
"license": "AGPL-3.0",
"description": "The open source Zapier alternative. Build workflow automation without spending time and money.",
"scripts": {
"dev": "ts-node-dev src/server.ts",
"worker": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/worker.ts",
@@ -17,10 +18,11 @@
"db:rollback": "knex migrate:rollback",
"db:migrate": "knex migrate:latest",
"copy-statics": "copyfiles src/**/*.{graphql,json,svg} dist",
"prepack": "yarn build"
"prepack": "yarn build",
"prebuild": "rm -rf ./dist"
},
"dependencies": {
"@automatisch/web": "0.1.0",
"@automatisch/web": "^0.1.4",
"@bull-board/express": "^3.10.1",
"@gitbeaker/node": "^35.6.0",
"@graphql-tools/graphql-file-loader": "^7.3.4",
@@ -98,7 +100,7 @@
"url": "https://github.com/automatisch/automatisch/issues"
},
"devDependencies": {
"@automatisch/types": "0.1.0",
"@automatisch/types": "^0.1.4",
"@types/bcrypt": "^5.0.0",
"@types/bull": "^3.15.8",
"@types/cors": "^2.8.12",
@@ -128,5 +130,8 @@
"require": [
"ts-node/register"
]
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -4,19 +4,19 @@ import type {
IField,
IJSONObject,
} from '@automatisch/types';
import HttpClient from '../../helpers/http-client';
import createHttpClient, { IHttpClient } from '../../helpers/http-client';
import { URLSearchParams } from 'url';
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
scopes: string[] = ['read:org', 'repo', 'user'];
client: HttpClient;
client: IHttpClient;
constructor(appData: IApp, connectionData: IJSONObject) {
this.connectionData = connectionData;
this.appData = appData;
this.client = new HttpClient({ baseURL: 'https://github.com' });
this.client = createHttpClient({ baseURL: 'https://github.com' });
}
get oauthRedirectUrl(): string {

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
}
]
}
]
}

View File

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

View File

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

View File

@@ -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 { data: [dateTimeObjectRepresentation] };
},
async testRun($: IGlobalVariable) {
const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
};

View File

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

View File

@@ -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 { data: [dateTimeObjectRepresentation] };
},
async testRun($: IGlobalVariable) {
const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
};

View File

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

View File

@@ -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 { data: [dateTimeObjectRepresentation] };
},
async testRun($: IGlobalVariable) {
const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
};

View File

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

View File

@@ -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 { data: [dateTimeObjectRepresentation] };
},
async testRun($: IGlobalVariable) {
const nextCronDateTime = getNextCronDateTime(this.getInterval($.db.step.parameters));
const dateTimeObjectRepresentation = getDateTimeObjectRepresentation(nextCronDateTime) as IJSONValue;
return { data: [dateTimeObjectRepresentation] };
},
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
aria-label="Slack" role="img"
viewBox="0 0 512 512"><rect
width="512" height="512"
rx="15%"
fill="#fff"/><g fill="#e01e5a"><path id="a" d="M149 305a39 39 0 01-78 0c0-22 17 -39 39 -39h39zM168 305a39 39 0 0178 0v97a39 39 0 01-78 0z"/></g><use xlink:href="#a" fill="#36c5f0" transform="rotate(90,256,256)"/><use xlink:href="#a" fill="#2eb67d" transform="rotate(180,256,256)"/><use xlink:href="#a" fill="#ecb22e" transform="rotate(270,256,256)"/></svg>
fill="#fff"/><g fill="#e01e5a"><path id="a" d="M149 305a39 39 0 01-78 0c0-22 17 -39 39 -39h39zM168 305a39 39 0 0178 0v97a39 39 0 01-78 0z"/></g><use xlink:href="#a" fill="#36c5f0" transform="rotate(90,256,256)"/><use xlink:href="#a" fill="#2eb67d" transform="rotate(180,256,256)"/><use xlink:href="#a" fill="#ecb22e" transform="rotate(270,256,256)"/></svg>

Before

Width:  |  Height:  |  Size: 533 B

After

Width:  |  Height:  |  Size: 531 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
import { IFlow, IStep, IConnection } from '@automatisch/types';
import HttpClient 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: HttpClient;
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 = new HttpClient({ baseURL: SlackClient.baseUrl });
this.verifyAccessToken = new VerifyAccessToken(this);
this.postMessageToChannel = new PostMessageToChannel(this);
this.findMessages = new FindMessages(this);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
}
]
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" aria-label="Twitter" role="img" viewBox="0 0 512 512">
<rect width="512" height="512" rx="15%" fill="#1da1f2"/>
<path fill="#fff" d="M437 152a72 72 0 01-40 12a72 72 0 0032-40a72 72 0 01-45 17a72 72 0 00-122 65a200 200 0 01-145-74a72 72 0 0022 94a72 72 0 01-32-7a72 72 0 0056 69a72 72 0 01-32 1a72 72 0 0067 50a200 200 0 01-105 29a200 200 0 00309-179a200 200 0 0035-37"/>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 422 B

After

Width:  |  Height:  |  Size: 421 B

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,64 +0,0 @@
import { IFlow, IStep, IConnection } from '@automatisch/types';
import OAuth from 'oauth-1.0a';
import crypto from 'crypto';
import HttpClient 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: HttpClient;
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 = new HttpClient({ 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);
}
}

View File

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

View File

@@ -0,0 +1,14 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
import generateRequest from './generate-request';
const getCurrentUser = async ($: IGlobalVariable): Promise<IJSONObject> => {
const response = await generateRequest($, {
requestPath: '/2/users/me',
method: 'GET',
});
const currentUser = response.data.data;
return currentUser;
};
export default getCurrentUser;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"
}
]
}
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,12 +5,12 @@ import type {
IJSONObject,
} from '@automatisch/types';
import { URLSearchParams } from 'url';
import HttpClient from '../../helpers/http-client';
import createHttpClient, { IHttpClient } from '../../helpers/http-client';
export default class Authentication implements IAuthentication {
appData: IApp;
connectionData: IJSONObject;
client: HttpClient;
client: IHttpClient;
scope: string[] = [
'forms:read',
@@ -25,7 +25,7 @@ export default class Authentication implements IAuthentication {
constructor(appData: IApp, connectionData: IJSONObject) {
this.connectionData = connectionData;
this.appData = appData;
this.client = new HttpClient({ baseURL: 'https://api.typeform.com' });
this.client = createHttpClient({ baseURL: 'https://api.typeform.com' });
}
get oauthRedirectUrl() {

View File

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

View File

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

View File

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

View File

@@ -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<string, unknown>;
parameters: IJSONObject;
flow: {
id: string;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<C>(data: C): C {
return JSON.parse(JSON.stringify(data));
}
async function getFileContent<C>(
path: string,
stripFuncs: boolean
): Promise<C> {
try {
const fileContent = await getDefaultExport(path);
if (stripFuncs) {
return stripFunctions(fileContent);
}
return fileContent;
} catch (err) {
return null;
}
}
async function getChildrenContentInDirectory<C>(
path: string,
stripFuncs: boolean
): Promise<C[]> {
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);
const fileContent = await getFileContent<C>(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<IAuth>(
`../apps/${appKey}/auth`,
stripFuncs
);
appData.triggers = await getChildrenContentInDirectory<ITrigger>(
`${appKey}/triggers`,
stripFuncs
);
appData.actions = await getChildrenContentInDirectory<IAction>(
`${appKey}/actions`,
stripFuncs
);
appData.data = await getChildrenContentInDirectory<IData>(
`${appKey}/data`,
stripFuncs
);
return appData;
};
export default getApp;

View File

@@ -0,0 +1,44 @@
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<IGlobalVariable> => {
const lastInternalId = await flow?.lastInternalId();
return {
auth: {
set: async (args: IJSONObject) => {
if (connection) {
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;

View File

@@ -1,34 +1,19 @@
import axios, { AxiosInstance, AxiosResponse } from 'axios';
import { IJSONObject, IHttpClientParams } from '@automatisch/types';
import axios from 'axios';
export { AxiosInstance as IHttpClient } from 'axios';
import { IHttpClientParams } from '@automatisch/types';
type ExtendedAxiosResponse = AxiosResponse & { integrationError: IJSONObject };
export default function createHttpClient({ baseURL }: IHttpClientParams) {
const instance = axios.create({
baseURL,
});
export default class HttpClient {
instance: AxiosInstance;
instance.interceptors.response.use(
(response) => response,
(error) => {
error.response.integrationError = error.response.data;
return error.response;
}
);
constructor(params: IHttpClientParams) {
this.instance = axios.create({
baseURL: params.baseURL,
});
this.instance.interceptors.response.use(
(response) => response,
(error) => {
error.response.integrationError = error.response.data;
return error.response;
}
);
}
async get(path: string, options?: IJSONObject) {
return (await this.instance.get(path, options)) as ExtendedAxiosResponse;
}
async post(path: string, body: IJSONObject | string, options?: IJSONObject) {
return (await this.instance.post(
path,
body,
options
)) as ExtendedAxiosResponse;
}
return instance;
}

View File

@@ -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<IApp[]> {
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<IApp> {
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<IApp> {
const rawAppData = await getApp(key, stripFuncs);
return appInfoConverter(rawAppData);
}
}

View File

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

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