Merge branch 'main' into issue-553
This commit is contained in:
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
@@ -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.
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
10
packages/backend/src/apps/scheduler/common/cron-times.ts
Normal file
10
packages/backend/src/apps/scheduler/common/cron-times.ts
Normal 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;
|
@@ -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,
|
||||
};
|
||||
}
|
@@ -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;
|
||||
};
|
@@ -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,
|
||||
};
|
||||
|
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
170
packages/backend/src/apps/scheduler/triggers/every-day/index.ts
Normal file
170
packages/backend/src/apps/scheduler/triggers/every-day/index.ts
Normal 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] };
|
||||
},
|
||||
};
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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] };
|
||||
},
|
||||
};
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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] };
|
||||
},
|
||||
};
|
@@ -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;
|
||||
}
|
||||
}
|
187
packages/backend/src/apps/scheduler/triggers/every-week/index.ts
Normal file
187
packages/backend/src/apps/scheduler/triggers/every-week/index.ts
Normal 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] };
|
||||
},
|
||||
};
|
@@ -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,
|
||||
};
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
@@ -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;
|
||||
},
|
||||
};
|
@@ -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;
|
||||
},
|
||||
};
|
@@ -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;
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 |
100
packages/backend/src/apps/slack/auth/index.ts
Normal file
100
packages/backend/src/apps/slack/auth/index.ts
Normal 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,
|
||||
};
|
12
packages/backend/src/apps/slack/auth/is-still-verified.ts
Normal file
12
packages/backend/src/apps/slack/auth/is-still-verified.ts
Normal 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;
|
34
packages/backend/src/apps/slack/auth/verify-credentials.ts
Normal file
34
packages/backend/src/apps/slack/auth/verify-credentials.ts
Normal 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;
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
41
packages/backend/src/apps/slack/data/list-channels/index.ts
Normal file
41
packages/backend/src/apps/slack/data/list-channels/index.ts
Normal 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;
|
||||
},
|
||||
};
|
@@ -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',
|
||||
};
|
||||
|
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
@@ -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];
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 |
35
packages/backend/src/apps/twitter/auth/create-auth-data.ts
Normal file
35
packages/backend/src/apps/twitter/auth/create-auth-data.ts
Normal 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}`
|
||||
);
|
||||
}
|
||||
}
|
219
packages/backend/src/apps/twitter/auth/index.ts
Normal file
219
packages/backend/src/apps/twitter/auth/index.ts
Normal 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,
|
||||
};
|
13
packages/backend/src/apps/twitter/auth/is-still-verified.ts
Normal file
13
packages/backend/src/apps/twitter/auth/is-still-verified.ts
Normal 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;
|
24
packages/backend/src/apps/twitter/auth/verify-credentials.ts
Normal file
24
packages/backend/src/apps/twitter/auth/verify-credentials.ts
Normal 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;
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
44
packages/backend/src/apps/twitter/common/generate-request.ts
Normal file
44
packages/backend/src/apps/twitter/common/generate-request.ts
Normal 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;
|
14
packages/backend/src/apps/twitter/common/get-current-user.ts
Normal file
14
packages/backend/src/apps/twitter/common/get-current-user.ts
Normal 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;
|
@@ -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;
|
@@ -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;
|
79
packages/backend/src/apps/twitter/common/get-user-tweets.ts
Normal file
79
packages/backend/src/apps/twitter/common/get-user-tweets.ts
Normal 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;
|
23
packages/backend/src/apps/twitter/common/oauth-client.ts
Normal file
23
packages/backend/src/apps/twitter/common/oauth-client.ts
Normal 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;
|
@@ -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',
|
||||
};
|
||||
|
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 });
|
||||
},
|
||||
};
|
@@ -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($);
|
||||
},
|
||||
};
|
@@ -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;
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
});
|
||||
},
|
||||
};
|
@@ -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;
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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,
|
||||
});
|
||||
},
|
||||
};
|
@@ -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() {
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
|
@@ -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,
|
||||
};
|
||||
|
@@ -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;
|
||||
};
|
||||
|
@@ -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,
|
||||
});
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
|
@@ -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')
|
||||
|
@@ -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;
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
78
packages/backend/src/helpers/get-app.ts
Normal file
78
packages/backend/src/helpers/get-app.ts
Normal 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;
|
44
packages/backend/src/helpers/global-variable.ts
Normal file
44
packages/backend/src/helpers/global-variable.ts
Normal 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;
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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
Reference in New Issue
Block a user