chore: Use get app helper to get application data

This commit is contained in:
Faruk AYDIN
2022-10-06 15:49:05 +03:00
parent a58a59788d
commit 0825eb36e4
52 changed files with 139 additions and 1761 deletions

View File

@@ -1,15 +0,0 @@
import SendMessageToChannel from './actions/send-message-to-channel';
import FindMessage from './actions/find-message';
import SlackClient from './client';
export default class Actions {
client: SlackClient;
sendMessageToChannel: SendMessageToChannel;
findMessage: FindMessage;
constructor(client: SlackClient) {
this.client = client;
this.sendMessageToChannel = new SendMessageToChannel(client);
this.findMessage = new FindMessage(client);
}
}

View File

@@ -1,26 +0,0 @@
import SlackClient from '../client';
export default class FindMessage {
client: SlackClient;
constructor(client: SlackClient) {
this.client = client;
}
async run() {
const parameters = this.client.step.parameters;
const query = parameters.query as string;
const sortBy = parameters.sortBy as string;
const sortDirection = parameters.sortDirection as string;
const count = 1;
const messages = await this.client.findMessages.run(
query,
sortBy,
sortDirection,
count,
);
return messages;
}
}

View File

@@ -1,18 +0,0 @@
import SlackClient from '../client';
export default class SendMessageToChannel {
client: SlackClient;
constructor(client: SlackClient) {
this.client = client;
}
async run() {
const channelId = this.client.step.parameters.channel as string;
const text = this.client.step.parameters.message as string;
const message = await this.client.postMessageToChannel.run(channelId, text);
return message;
}
}

View File

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

View File

@@ -1,36 +0,0 @@
import type { IAuthentication, IJSONObject } from '@automatisch/types';
import SlackClient from './client';
export default class Authentication implements IAuthentication {
client: SlackClient;
static requestOptions: IJSONObject = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
constructor(client: SlackClient) {
this.client = client;
}
async verifyCredentials() {
const { bot_id: botId, user: screenName } =
await this.client.verifyAccessToken.run();
return {
botId,
screenName,
token: this.client.connection.formattedData.accessToken,
};
}
async isStillVerified() {
try {
await this.client.verifyAccessToken.run();
return true;
} catch (error) {
return false;
}
}
}

View File

@@ -1,44 +0,0 @@
import SlackClient from '../index';
export default class FindMessages {
client: SlackClient;
constructor(client: SlackClient) {
this.client = client;
}
async run(query: string, sortBy: string, sortDirection: string, count = 1) {
const headers = {
Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
};
const params = {
query,
sort: sortBy,
sort_dir: sortDirection,
count,
};
const response = await this.client.httpClient.get('/search.messages', {
headers,
params,
});
const data = response.data;
if (!data.ok) {
if (data.error === 'missing_scope') {
throw new Error(
`Error occured while finding messages; ${data.error}: ${data.needed}`
);
}
throw new Error(`Error occured while finding messages; ${data.error}`);
}
const messages = data.messages.matches;
const message = messages?.[0];
return message;
}
}

View File

@@ -1,44 +0,0 @@
import SlackClient from '../index';
import { IJSONObject } from '@automatisch/types';
export default class PostMessageToChannel {
client: SlackClient;
constructor(client: SlackClient) {
this.client = client;
}
async run(channelId: string, text: string) {
const message: {
data: IJSONObject | null;
error: IJSONObject | null;
} = {
data: null,
error: null,
};
const headers = {
Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
};
const params = {
channel: channelId,
text,
};
const response = await this.client.httpClient.post(
'/chat.postMessage',
params,
{ headers }
);
message.error = response?.integrationError;
message.data = response?.data?.message;
if (response.data.ok === false) {
message.error = response.data;
}
return message;
}
}

View File

@@ -1,35 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import qs from 'qs';
import SlackClient from '../index';
export default class VerifyAccessToken {
client: SlackClient;
static requestOptions: IJSONObject = {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
};
constructor(client: SlackClient) {
this.client = client;
}
async run() {
const response = await this.client.httpClient.post(
'/auth.test',
qs.stringify({
token: this.client.connection.formattedData.accessToken,
}),
VerifyAccessToken.requestOptions
);
if (response.data.ok === false) {
throw new Error(
`Error occured while verifying credentials: ${response.data.error}.(More info: https://api.slack.com/methods/auth.test#errors)`
);
}
return response.data;
}
}

View File

@@ -1,29 +0,0 @@
import { IFlow, IStep, IConnection } from '@automatisch/types';
import 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);
}
}

View File

@@ -1,12 +0,0 @@
import ListChannels from './data/list-channels';
import SlackClient from './client';
export default class Data {
client: SlackClient;
listChannels: ListChannels;
constructor(client: SlackClient) {
this.client = client;
this.listChannels = new ListChannels(client);
}
}

View File

@@ -1,31 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import SlackClient from '../client';
export default class ListChannels {
client: SlackClient;
constructor(client: SlackClient) {
this.client = client;
}
async run() {
const response = await this.client.httpClient.get('/conversations.list', {
headers: {
Authorization: `Bearer ${this.client.connection.formattedData.accessToken}`,
},
});
if (response.data.ok === 'false') {
throw new Error(
`Error occured while fetching slack channels: ${response.data.error}`
);
}
return response.data.channels.map((channel: IJSONObject) => {
return {
value: channel.id,
name: channel.name,
};
});
}
}

View File

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

View File

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

View File

@@ -1,13 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import NewMessageToChannel from './triggers/new-message-to-channel';
export default class Triggers {
newMessageToChannel: NewMessageToChannel;
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.newMessageToChannel = new NewMessageToChannel(
connectionData,
parameters
);
}
}

View File

@@ -1,47 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import axios, { AxiosInstance } from 'axios';
export default class NewMessageToChannel {
httpClient: AxiosInstance;
parameters: IJSONObject;
connectionData: IJSONObject;
BASE_URL = 'https://slack.com/api';
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
this.httpClient = axios.create({ baseURL: this.BASE_URL });
this.connectionData = connectionData;
this.parameters = parameters;
}
async run() {
// TODO: Fix after webhook implementation.
}
async testRun() {
const headers = {
Authorization: `Bearer ${this.connectionData.accessToken}`,
};
const params = {
channel: this.parameters.channel,
};
const response = await this.httpClient.get('/conversations.history', {
headers,
params,
});
let lastMessage;
if (this.parameters.triggerForBotMessages) {
lastMessage = response.data.messages[0];
} else {
lastMessage = response.data.messages.find(
(message: IJSONObject) =>
!Object.prototype.hasOwnProperty.call(message, 'bot_id')
);
}
return [lastMessage];
}
}

View File

@@ -1,12 +0,0 @@
import TwitterClient from './client';
import CreateTweet from './actions/create-tweet';
export default class Actions {
client: TwitterClient;
createTweet: CreateTweet;
constructor(client: TwitterClient) {
this.client = client;
this.createTweet = new CreateTweet(client);
}
}

View File

@@ -1,17 +0,0 @@
import TwitterClient from '../client';
export default class CreateTweet {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run() {
const tweet = await this.client.createTweet.run(
this.client.step.parameters.tweet as string
);
return tweet;
}
}

View File

@@ -1,4 +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

View File

@@ -1,51 +0,0 @@
import type { IAuthentication, IField } from '@automatisch/types';
import { URLSearchParams } from 'url';
import TwitterClient from './client';
export default class Authentication implements IAuthentication {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async createAuthData() {
const appFields = this.client.connection.appData.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const callbackUrl = appFields.value;
const response = await this.client.oauthRequestToken.run(callbackUrl);
const responseData = Object.fromEntries(new URLSearchParams(response.data));
return {
url: `${TwitterClient.baseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}`,
accessToken: responseData.oauth_token,
accessSecret: responseData.oauth_token_secret,
};
}
async verifyCredentials() {
const response = await this.client.verifyAccessToken.run();
const responseData = Object.fromEntries(new URLSearchParams(response.data));
return {
consumerKey: this.client.connection.formattedData.consumerKey as string,
consumerSecret: this.client.connection.formattedData
.consumerSecret as string,
accessToken: responseData.oauth_token,
accessSecret: responseData.oauth_token_secret,
userId: responseData.user_id,
screenName: responseData.screen_name,
};
}
async isStillVerified() {
try {
await this.client.getCurrentUser.run();
return true;
} catch (error) {
return false;
}
}
}

View File

@@ -1,40 +0,0 @@
import TwitterClient from '../index';
export default class CreateTweet {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(text: string) {
try {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
const requestData = {
url: `${TwitterClient.baseUrl}/2/tweets`,
method: 'POST',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
const response = await this.client.httpClient.post(
`/2/tweets`,
{ text },
{ headers: { ...authHeader } }
);
const tweet = response.data.data;
return tweet;
} catch (error) {
const errorMessage = error.response.data.detail;
throw new Error(`Error occured while creating a tweet: ${errorMessage}`);
}
}
}

View File

@@ -1,35 +0,0 @@
import TwitterClient from '../index';
export default class GetCurrentUser {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run() {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
const requestPath = '/2/users/me';
const requestData = {
url: `${TwitterClient.baseUrl}${requestPath}`,
method: 'GET',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
const response = await this.client.httpClient.get(requestPath, {
headers: { ...authHeader },
});
const currentUser = response.data.data;
return currentUser;
}
}

View File

@@ -1,45 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import TwitterClient from '../index';
export default class GetUserByUsername {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(username: string) {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
const requestPath = `/2/users/by/username/${username}`;
const requestData = {
url: `${TwitterClient.baseUrl}${requestPath}`,
method: 'GET',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
const response = await this.client.httpClient.get(requestPath, {
headers: { ...authHeader },
});
if (response.data?.errors) {
const errorMessages = response.data.errors
.map((error: IJSONObject) => error.detail)
.join(' ');
throw new Error(
`Error occured while fetching user data: ${errorMessages}`
);
}
const user = response.data.data;
return user;
}
}

View File

@@ -1,70 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import { URLSearchParams } from 'url';
import TwitterClient from '../index';
import omitBy from 'lodash/omitBy';
import isEmpty from 'lodash/isEmpty';
export default class GetUserFollowers {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(userId: string, lastInternalId?: string) {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
let response;
const followers: IJSONObject[] = [];
do {
const params: IJSONObject = {
pagination_token: response?.data?.meta?.next_token,
};
const queryParams = new URLSearchParams(omitBy(params, isEmpty));
const requestPath = `/2/users/${userId}/followers${
queryParams.toString() ? `?${queryParams.toString()}` : ''
}`;
const requestData = {
url: `${TwitterClient.baseUrl}${requestPath}`,
method: 'GET',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
response = await this.client.httpClient.get(requestPath, {
headers: { ...authHeader },
});
if (response.data.meta.result_count > 0) {
response.data.data.forEach((tweet: IJSONObject) => {
if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
followers.push(tweet);
} else {
return;
}
});
}
} while (response.data.meta.next_token && lastInternalId);
if (response.data?.errors) {
const errorMessages = response.data.errors
.map((error: IJSONObject) => error.detail)
.join(' ');
throw new Error(
`Error occured while fetching user data: ${errorMessages}`
);
}
return followers;
}
}

View File

@@ -1,71 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import { URLSearchParams } from 'url';
import TwitterClient from '../index';
import omitBy from 'lodash/omitBy';
import isEmpty from 'lodash/isEmpty';
export default class GetUserTweets {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(userId: string, lastInternalId?: string) {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
let response;
const tweets: IJSONObject[] = [];
do {
const params: IJSONObject = {
since_id: lastInternalId,
pagination_token: response?.data?.meta?.next_token,
};
const queryParams = new URLSearchParams(omitBy(params, isEmpty));
const requestPath = `/2/users/${userId}/tweets${
queryParams.toString() ? `?${queryParams.toString()}` : ''
}`;
const requestData = {
url: `${TwitterClient.baseUrl}${requestPath}`,
method: 'GET',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
response = await this.client.httpClient.get(requestPath, {
headers: { ...authHeader },
});
if (response.data.meta.result_count > 0) {
response.data.data.forEach((tweet: IJSONObject) => {
if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
tweets.push(tweet);
} else {
return;
}
});
}
} while (response.data.meta.next_token && lastInternalId);
if (response.data?.errors) {
const errorMessages = response.data.errors
.map((error: IJSONObject) => error.detail)
.join(' ');
throw new Error(
`Error occured while fetching user data: ${errorMessages}`
);
}
return tweets;
}
}

View File

@@ -1,42 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import TwitterClient from '../index';
export default class OAuthRequestToken {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(callbackUrl: string) {
try {
const requestData = {
url: `${TwitterClient.baseUrl}/oauth/request_token`,
method: 'POST',
data: { oauth_callback: callbackUrl },
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData)
);
const response = await this.client.httpClient.post(
`/oauth/request_token`,
null,
{
headers: { ...authHeader },
}
);
return response;
} catch (error) {
const errorMessages = error.response.data.errors
.map((error: IJSONObject) => error.message)
.join(' ');
throw new Error(
`Error occured while verifying credentials: ${errorMessages}`
);
}
}
}

View File

@@ -1,74 +0,0 @@
import { IJSONObject } from '@automatisch/types';
import { URLSearchParams } from 'url';
import TwitterClient from '../index';
import omitBy from 'lodash/omitBy';
import isEmpty from 'lodash/isEmpty';
import qs from 'qs';
export default class SearchTweets {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(searchTerm: string, lastInternalId?: string) {
const token = {
key: this.client.connection.formattedData.accessToken as string,
secret: this.client.connection.formattedData.accessSecret as string,
};
let response;
const tweets: {
data: IJSONObject[];
error: IJSONObject | null;
} = {
data: [],
error: null,
};
do {
const params: IJSONObject = {
query: searchTerm,
since_id: lastInternalId,
pagination_token: response?.data?.meta?.next_token,
};
const queryParams = qs.stringify(omitBy(params, isEmpty));
const requestPath = `/2/tweets/search/recent${
queryParams.toString() ? `?${queryParams.toString()}` : ''
}`;
const requestData = {
url: `${TwitterClient.baseUrl}${requestPath}`,
method: 'GET',
};
const authHeader = this.client.oauthClient.toHeader(
this.client.oauthClient.authorize(requestData, token)
);
response = await this.client.httpClient.get(requestPath, {
headers: { ...authHeader },
});
if (response.integrationError) {
tweets.error = response.integrationError;
return tweets;
}
if (response.data.meta.result_count > 0) {
response.data.data.forEach((tweet: IJSONObject) => {
if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) {
tweets.data.push(tweet);
} else {
return;
}
});
}
} while (response.data.meta.next_token && lastInternalId);
return tweets;
}
}

View File

@@ -1,20 +0,0 @@
import TwitterClient from '../index';
export default class VerifyAccessToken {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run() {
try {
return await this.client.httpClient.post(
`/oauth/access_token?oauth_verifier=${this.client.connection.formattedData.oauthVerifier}&oauth_token=${this.client.connection.formattedData.accessToken}`,
null
);
} catch (error) {
throw new Error(error.response.data);
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,21 +0,0 @@
import TwitterClient from './client';
import UserTweets from './triggers/user-tweets';
import SearchTweets from './triggers/search-tweets';
import MyTweets from './triggers/my-tweets';
import MyFollowers from './triggers/my-followers';
export default class Triggers {
client: TwitterClient;
userTweets: UserTweets;
searchTweets: SearchTweets;
myTweets: MyTweets;
myFollowers: MyFollowers;
constructor(client: TwitterClient) {
this.client = client;
this.userTweets = new UserTweets(client);
this.searchTweets = new SearchTweets(client);
this.myTweets = new MyTweets(client);
this.myFollowers = new MyFollowers(client);
}
}

View File

@@ -1,28 +0,0 @@
import TwitterClient from '../client';
export default class MyFollowers {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(lastInternalId: string) {
return this.getFollowers(lastInternalId);
}
async testRun() {
return this.getFollowers();
}
async getFollowers(lastInternalId?: string) {
const { username } = await this.client.getCurrentUser.run();
const user = await this.client.getUserByUsername.run(username as string);
const tweets = await this.client.getUserFollowers.run(
user.id,
lastInternalId
);
return tweets;
}
}

View File

@@ -1,25 +0,0 @@
import TwitterClient from '../client';
export default class MyTweets {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run() {
return this.getTweets(lastInternalId);
}
async testRun() {
return this.getTweets();
}
async getTweets(lastInternalId?: string) {
const { username } = await this.client.getCurrentUser.run();
const user = await this.client.getUserByUsername.run(username as string);
const tweets = await this.client.getUserTweets.run(user.id, lastInternalId);
return tweets;
}
}

View File

@@ -1,26 +0,0 @@
import TwitterClient from '../client';
export default class SearchTweets {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(lastInternalId: string) {
return this.getTweets(lastInternalId);
}
async testRun() {
return this.getTweets();
}
async getTweets(lastInternalId?: string) {
const tweets = await this.client.searchTweets.run(
this.client.step.parameters.searchTerm as string,
lastInternalId
);
return tweets;
}
}

View File

@@ -1,27 +0,0 @@
import TwitterClient from '../client';
export default class UserTweets {
client: TwitterClient;
constructor(client: TwitterClient) {
this.client = client;
}
async run(lastInternalId: string) {
return this.getTweets(lastInternalId);
}
async testRun() {
return this.getTweets();
}
async getTweets(lastInternalId?: string) {
const user = await this.client.getUserByUsername.run(
this.client.step.parameters.username as string
);
const tweets = await this.client.getUserTweets.run(user.id, lastInternalId);
return tweets;
}
}

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ const createConnection = async (
params: Params,
context: Context
) => {
App.findOneByKey(params.input.key);
await App.findOneByKey(params.input.key);
return await context.currentUser.$relatedQuery('connections').insert({
key: params.input.key,

View File

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

View File

@@ -6,7 +6,7 @@ type Params = {
};
const getApp = async (_parent: unknown, params: Params, context: Context) => {
const app = App.findOneByKey(params.key);
const app = await App.findOneByKey(params.key);
if (context.currentUser) {
const connections = await context.currentUser

View File

@@ -6,8 +6,8 @@ type Params = {
onlyWithTriggers: boolean;
};
const getApps = (_parent: unknown, params: Params) => {
const apps = App.findAll(params.name);
const getApps = async (_parent: unknown, params: Params) => {
const apps = await App.findAll(params.name);
if (params.onlyWithTriggers) {
return apps.filter((app: IApp) => app.triggers?.length);

View File

@@ -11,7 +11,7 @@ const getConnectedApps = async (
params: Params,
context: Context
) => {
let apps = App.findAll(params.name);
let apps = await App.findAll(params.name);
const connections = await context.currentUser
.$relatedQuery('connections')

View File

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

View File

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

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

View File

@@ -2,6 +2,7 @@ import fs from 'fs';
import { join } from 'path';
import { IApp } from '@automatisch/types';
import appInfoConverter from '../helpers/app-info-converter';
import getApp from '../helpers/get-app';
class App {
static folderPath = join(__dirname, '../apps');
@@ -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);
}
}

View File

@@ -56,10 +56,6 @@ class Connection extends Base {
},
});
get appData() {
return App.findOneByKey(this.key);
}
encryptData(): void {
if (!this.eligibleForEncryption()) return;

View File

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

View File

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