chore: Use get app helper to get application data
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
@@ -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 +0,0 @@
|
||||
|
||||
<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>
|
Before Width: | Height: | Size: 533 B |
@@ -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 createHttpClient, { IHttpClient } from '../../../helpers/http-client';
|
||||
import VerifyAccessToken from './endpoints/verify-access-token';
|
||||
import PostMessageToChannel from './endpoints/post-message-to-channel';
|
||||
import FindMessages from './endpoints/find-messages';
|
||||
|
||||
export default class SlackClient {
|
||||
flow: IFlow;
|
||||
step: IStep;
|
||||
connection: IConnection;
|
||||
httpClient: IHttpClient;
|
||||
|
||||
verifyAccessToken: VerifyAccessToken;
|
||||
postMessageToChannel: PostMessageToChannel;
|
||||
findMessages: FindMessages;
|
||||
|
||||
static baseUrl = 'https://slack.com/api';
|
||||
|
||||
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
|
||||
this.connection = connection;
|
||||
this.flow = flow;
|
||||
this.step = step;
|
||||
|
||||
this.httpClient = createHttpClient({ baseURL: SlackClient.baseUrl });
|
||||
this.verifyAccessToken = new VerifyAccessToken(this);
|
||||
this.postMessageToChannel = new PostMessageToChannel(this);
|
||||
this.findMessages = new FindMessages(this);
|
||||
}
|
||||
}
|
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,30 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@@ -1,278 +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,
|
||||
"baseUrl": "https://slack.com/api",
|
||||
"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 +0,0 @@
|
||||
<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>
|
Before Width: | Height: | Size: 422 B |
@@ -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 createHttpClient, { IHttpClient } from '../../../helpers/http-client';
|
||||
import OAuthRequestToken from './endpoints/oauth-request-token';
|
||||
import VerifyAccessToken from './endpoints/verify-access-token';
|
||||
import GetCurrentUser from './endpoints/get-current-user';
|
||||
import GetUserByUsername from './endpoints/get-user-by-username';
|
||||
import GetUserTweets from './endpoints/get-user-tweets';
|
||||
import CreateTweet from './endpoints/create-tweet';
|
||||
import SearchTweets from './endpoints/search-tweets';
|
||||
import GetUserFollowers from './endpoints/get-user-followers';
|
||||
|
||||
export default class TwitterClient {
|
||||
flow: IFlow;
|
||||
step: IStep;
|
||||
connection: IConnection;
|
||||
oauthClient: OAuth;
|
||||
httpClient: IHttpClient;
|
||||
|
||||
oauthRequestToken: OAuthRequestToken;
|
||||
verifyAccessToken: VerifyAccessToken;
|
||||
getCurrentUser: GetCurrentUser;
|
||||
getUserByUsername: GetUserByUsername;
|
||||
getUserTweets: GetUserTweets;
|
||||
createTweet: CreateTweet;
|
||||
searchTweets: SearchTweets;
|
||||
getUserFollowers: GetUserFollowers;
|
||||
|
||||
static baseUrl = 'https://api.twitter.com';
|
||||
|
||||
constructor(connection: IConnection, flow?: IFlow, step?: IStep) {
|
||||
this.connection = connection;
|
||||
this.flow = flow;
|
||||
this.step = step;
|
||||
|
||||
this.httpClient = createHttpClient({ baseURL: TwitterClient.baseUrl });
|
||||
|
||||
const consumerData = {
|
||||
key: this.connection.formattedData.consumerKey as string,
|
||||
secret: this.connection.formattedData.consumerSecret as string,
|
||||
};
|
||||
|
||||
this.oauthClient = new OAuth({
|
||||
consumer: consumerData,
|
||||
signature_method: 'HMAC-SHA1',
|
||||
hash_function(base_string, key) {
|
||||
return crypto
|
||||
.createHmac('sha1', key)
|
||||
.update(base_string)
|
||||
.digest('base64');
|
||||
},
|
||||
});
|
||||
|
||||
this.oauthRequestToken = new OAuthRequestToken(this);
|
||||
this.verifyAccessToken = new VerifyAccessToken(this);
|
||||
this.getCurrentUser = new GetCurrentUser(this);
|
||||
this.getUserByUsername = new GetUserByUsername(this);
|
||||
this.getUserTweets = new GetUserTweets(this);
|
||||
this.createTweet = new CreateTweet(this);
|
||||
this.searchTweets = new SearchTweets(this);
|
||||
this.getUserFollowers = new GetUserFollowers(this);
|
||||
}
|
||||
}
|
@@ -1,27 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
@@ -1,339 +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,
|
||||
"baseUrl": "https://api.twitter.com",
|
||||
"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() {
|
||||
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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@ import { URLSearchParams } from 'url';
|
||||
|
||||
export default async function createAuthData($: IGlobalVariable) {
|
||||
try {
|
||||
const oauthRedirectUrlField = $.app.fields.find(
|
||||
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||
);
|
||||
|
||||
|
@@ -1,17 +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";
|
||||
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 myFollowers = async ($: IGlobalVariable, lastInternalId?: string) => {
|
||||
const { username } = await getCurrentUser($);
|
||||
const user = await getUserByUsername($, username as string);
|
||||
|
||||
const tweets = await getUserFollowers($, {
|
||||
userId: user.id,
|
||||
lastInternalId
|
||||
lastInternalId,
|
||||
});
|
||||
return tweets;
|
||||
});
|
||||
};
|
||||
|
||||
export default myFollowers;
|
||||
|
@@ -27,7 +27,7 @@ const createAuthData = async (
|
||||
|
||||
const authInstance = (await import(`../../apps/${connection.key}2/auth`))
|
||||
.default;
|
||||
const app = App.findOneByKey(connection.key);
|
||||
const app = await App.findOneByKey(connection.key);
|
||||
|
||||
const $ = globalVariable(connection, app);
|
||||
await authInstance.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,
|
||||
|
@@ -20,7 +20,7 @@ const verifyConnection = async (
|
||||
})
|
||||
.throwIfNotFound();
|
||||
|
||||
const app = App.findOneByKey(connection.key);
|
||||
const app = await App.findOneByKey(connection.key);
|
||||
const authInstance = (await import(`../../apps/${connection.key}2/auth`))
|
||||
.default;
|
||||
|
||||
|
@@ -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')
|
||||
|
@@ -2,10 +2,10 @@ import express, { Application } from 'express';
|
||||
import App from '../models/app';
|
||||
|
||||
const appAssetsHandler = async (app: Application) => {
|
||||
const appNames = App.list;
|
||||
const appList = await App.findAll();
|
||||
|
||||
appNames.forEach(appName => {
|
||||
const svgPath = `${__dirname}/../apps/${appName}/assets/favicon.svg`;
|
||||
appList.forEach((appItem) => {
|
||||
const svgPath = `${__dirname}/../apps/${appItem.name}/assets/favicon.svg`;
|
||||
const staticFileHandlerOptions = {
|
||||
/**
|
||||
* Disabling fallthrough is important to respond with HTTP 404.
|
||||
@@ -15,11 +15,8 @@ const appAssetsHandler = async (app: Application) => {
|
||||
};
|
||||
const staticFileHandler = express.static(svgPath, staticFileHandlerOptions);
|
||||
|
||||
app.use(
|
||||
`/apps/${appName}/assets/favicon.svg`,
|
||||
staticFileHandler
|
||||
)
|
||||
})
|
||||
}
|
||||
app.use(`/apps/${appItem.name}/assets/favicon.svg`, staticFileHandler);
|
||||
});
|
||||
};
|
||||
|
||||
export default appAssetsHandler;
|
||||
|
@@ -1,12 +1,20 @@
|
||||
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) => {
|
||||
const stringifiedRawAppData = JSON.stringify(rawAppData);
|
||||
|
||||
const computedJSONData: IApp = JSON.parse(computedRawData)
|
||||
let computedRawData = stringifiedRawAppData.replace(
|
||||
'{BASE_URL}',
|
||||
appConfig.baseUrl
|
||||
);
|
||||
computedRawData = computedRawData.replace(
|
||||
'{WEB_APP_URL}',
|
||||
appConfig.webAppUrl
|
||||
);
|
||||
|
||||
const computedJSONData: IApp = JSON.parse(computedRawData);
|
||||
return computedJSONData;
|
||||
}
|
||||
};
|
||||
|
||||
export default appInfoConverter;
|
||||
|
67
packages/backend/src/helpers/get-app.ts
Normal file
67
packages/backend/src/helpers/get-app.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import fs from 'fs';
|
||||
import { join } from 'path';
|
||||
import { IApp } from '@automatisch/types';
|
||||
|
||||
const folderPath = join(__dirname, '../apps');
|
||||
|
||||
const getApp = async (appKey: string) => {
|
||||
const appData: IApp = (await import(`../apps/${appKey}`)).default;
|
||||
|
||||
let rawAuthData = (await import(`../apps/${appKey}/auth/index.ts`)).default;
|
||||
|
||||
rawAuthData = Object.fromEntries(
|
||||
Object.entries(rawAuthData).filter(
|
||||
([key]) => typeof rawAuthData[key] !== 'function'
|
||||
)
|
||||
);
|
||||
|
||||
appData.auth = rawAuthData;
|
||||
|
||||
appData.triggers = [];
|
||||
|
||||
const triggersPath = join(folderPath, appKey, 'triggers');
|
||||
|
||||
if (fs.existsSync(triggersPath)) {
|
||||
const triggersFolder = fs.readdirSync(join(folderPath, appKey, 'triggers'));
|
||||
|
||||
for (const triggerName of triggersFolder) {
|
||||
let rawTriggerData = (
|
||||
await import(`../apps/${appKey}/triggers/${triggerName}/index.ts`)
|
||||
).default;
|
||||
|
||||
rawTriggerData = Object.fromEntries(
|
||||
Object.entries(rawTriggerData).filter(
|
||||
([key]) => typeof rawTriggerData[key] !== 'function'
|
||||
)
|
||||
);
|
||||
|
||||
appData.triggers.push(rawTriggerData);
|
||||
}
|
||||
}
|
||||
|
||||
appData.actions = [];
|
||||
|
||||
const actionsPath = join(folderPath, appKey, 'actions');
|
||||
|
||||
if (fs.existsSync(actionsPath)) {
|
||||
const actionsFolder = fs.readdirSync(join(folderPath, appKey, 'actions'));
|
||||
|
||||
for await (const actionName of actionsFolder) {
|
||||
let rawActionData = (
|
||||
await import(`../apps/${appKey}/actions/${actionName}/index.ts`)
|
||||
).default;
|
||||
|
||||
rawActionData = Object.fromEntries(
|
||||
Object.entries(rawActionData).filter(
|
||||
([key]) => typeof rawActionData[key] !== 'function'
|
||||
)
|
||||
);
|
||||
|
||||
appData.actions.push(rawActionData);
|
||||
}
|
||||
}
|
||||
|
||||
return appData;
|
||||
};
|
||||
|
||||
export default getApp;
|
@@ -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');
|
||||
@@ -9,30 +10,29 @@ class App {
|
||||
|
||||
// Temporaryly restrict the apps we expose until
|
||||
// their actions/triggers are implemented!
|
||||
static temporaryList = ['slack', 'twitter', 'scheduler'];
|
||||
static temporaryList = ['slack2', 'twitter2'];
|
||||
// static temporaryList = ['slack', 'twitter', 'scheduler'];
|
||||
|
||||
static findAll(name?: string): IApp[] {
|
||||
static async findAll(name?: string): Promise<IApp[]> {
|
||||
if (!name)
|
||||
return this.temporaryList.map((name) => this.findOneByName(name));
|
||||
return Promise.all(
|
||||
this.temporaryList.map(async (name) => await this.findOneByName(name))
|
||||
);
|
||||
|
||||
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))
|
||||
);
|
||||
}
|
||||
|
||||
static findOneByName(name: string): IApp {
|
||||
const rawAppData = fs.readFileSync(
|
||||
this.folderPath + `/${name}/info.json`,
|
||||
'utf-8'
|
||||
);
|
||||
static async findOneByName(name: string): Promise<IApp> {
|
||||
const rawAppData = await getApp(name.toLocaleLowerCase());
|
||||
return appInfoConverter(rawAppData);
|
||||
}
|
||||
|
||||
static findOneByKey(key: string): IApp {
|
||||
const rawAppData = fs.readFileSync(
|
||||
this.folderPath + `/${key}/info.json`,
|
||||
'utf-8'
|
||||
);
|
||||
static async findOneByKey(key: string): Promise<IApp> {
|
||||
const rawAppData = await getApp(key);
|
||||
return appInfoConverter(rawAppData);
|
||||
}
|
||||
}
|
||||
|
@@ -56,10 +56,6 @@ class Connection extends Base {
|
||||
},
|
||||
});
|
||||
|
||||
get appData() {
|
||||
return App.findOneByKey(this.key);
|
||||
}
|
||||
|
||||
encryptData(): void {
|
||||
if (!this.eligibleForEncryption()) return;
|
||||
|
||||
|
@@ -78,10 +78,6 @@ class Step extends Base {
|
||||
return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`;
|
||||
}
|
||||
|
||||
get appData() {
|
||||
return App.findOneByKey(this.appKey);
|
||||
}
|
||||
|
||||
async $afterInsert(queryContext: QueryContext) {
|
||||
await super.$afterInsert(queryContext);
|
||||
Telemetry.stepCreated(this);
|
||||
|
27
packages/types/index.d.ts
vendored
27
packages/types/index.d.ts
vendored
@@ -158,14 +158,18 @@ export interface IApp {
|
||||
primaryColor: string;
|
||||
supportsConnections: boolean;
|
||||
baseUrl: string;
|
||||
auth: IAuth;
|
||||
connectionCount: number;
|
||||
flowCount: number;
|
||||
triggers: ITrigger[];
|
||||
actions: IAction[];
|
||||
connections: IConnection[];
|
||||
}
|
||||
|
||||
export interface IAuth {
|
||||
fields: IField[];
|
||||
authenticationSteps: IAuthenticationStep[];
|
||||
reconnectionSteps: IAuthenticationStep[];
|
||||
connectionCount: number;
|
||||
flowCount: number;
|
||||
triggers: any[];
|
||||
actions: any[];
|
||||
connections: IConnection[];
|
||||
}
|
||||
|
||||
export interface IService {
|
||||
@@ -176,10 +180,23 @@ export interface IService {
|
||||
}
|
||||
|
||||
export interface ITrigger {
|
||||
name: string;
|
||||
key: string,
|
||||
pollInterval: number;
|
||||
description: string;
|
||||
substeps: ISubstep[];
|
||||
run(startTime?: Date): Promise<IJSONValue>;
|
||||
testRun(startTime?: Date): Promise<IJSONValue>;
|
||||
}
|
||||
|
||||
export interface IAction {
|
||||
name: string;
|
||||
key: string,
|
||||
description: string;
|
||||
substeps: ISubstep[];
|
||||
run(startTime?: Date): Promise<IJSONValue>;
|
||||
}
|
||||
|
||||
export interface IAuthentication {
|
||||
client: unknown;
|
||||
verifyCredentials(): Promise<IJSONObject>;
|
||||
|
Reference in New Issue
Block a user