Merge pull request #435 from automatisch/refactor/user-tweet-trigger
refactor: Adjust architecture for twitter client and user tweet trigger
This commit is contained in:
@@ -1,96 +1,37 @@
|
|||||||
import type {
|
import type { IAuthentication, IField } from '@automatisch/types';
|
||||||
IAuthentication,
|
|
||||||
IApp,
|
|
||||||
IField,
|
|
||||||
IJSONObject,
|
|
||||||
} from '@automatisch/types';
|
|
||||||
import HttpClient from '../../helpers/http-client';
|
|
||||||
import OAuth from 'oauth-1.0a';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
import { URLSearchParams } from 'url';
|
import { URLSearchParams } from 'url';
|
||||||
|
import TwitterClient from './client';
|
||||||
|
|
||||||
export default class Authentication implements IAuthentication {
|
export default class Authentication implements IAuthentication {
|
||||||
appData: IApp;
|
client: TwitterClient;
|
||||||
connectionData: IJSONObject;
|
|
||||||
|
|
||||||
client: HttpClient;
|
constructor(client: TwitterClient) {
|
||||||
oauthClient: OAuth;
|
this.client = client;
|
||||||
|
|
||||||
static baseUrl = 'https://api.twitter.com';
|
|
||||||
|
|
||||||
constructor(appData: IApp, connectionData: IJSONObject) {
|
|
||||||
this.appData = appData;
|
|
||||||
this.connectionData = connectionData;
|
|
||||||
this.client = new HttpClient({ baseURL: Authentication.baseUrl });
|
|
||||||
this.oauthClient = new OAuth({
|
|
||||||
consumer: {
|
|
||||||
key: this.connectionData.consumerKey as string,
|
|
||||||
secret: this.connectionData.consumerSecret as string,
|
|
||||||
},
|
|
||||||
signature_method: 'HMAC-SHA1',
|
|
||||||
hash_function(base_string, key) {
|
|
||||||
return crypto
|
|
||||||
.createHmac('sha1', key)
|
|
||||||
.update(base_string)
|
|
||||||
.digest('base64');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async createAuthData() {
|
async createAuthData() {
|
||||||
const appFields = this.appData.fields.find(
|
const appFields = this.client.appData.fields.find(
|
||||||
(field: IField) => field.key == 'oAuthRedirectUrl'
|
(field: IField) => field.key == 'oAuthRedirectUrl'
|
||||||
);
|
);
|
||||||
const callbackUrl = appFields.value;
|
const callbackUrl = appFields.value;
|
||||||
|
|
||||||
const requestData = {
|
const response = await this.client.oauthRequestToken.run(callbackUrl);
|
||||||
url: `${Authentication.baseUrl}/oauth/request_token`,
|
const responseData = Object.fromEntries(new URLSearchParams(response.data));
|
||||||
method: 'POST',
|
|
||||||
data: { oauth_callback: callbackUrl },
|
return {
|
||||||
|
url: `${TwitterClient.baseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}`,
|
||||||
|
accessToken: responseData.oauth_token,
|
||||||
|
accessSecret: responseData.oauth_token_secret,
|
||||||
};
|
};
|
||||||
|
|
||||||
const authHeader = this.oauthClient.toHeader(
|
|
||||||
this.oauthClient.authorize(requestData)
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await this.client.post(`/oauth/request_token`, null, {
|
|
||||||
headers: { ...authHeader },
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseData = Object.fromEntries(
|
|
||||||
new URLSearchParams(response.data)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
url: `${Authentication.baseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}`,
|
|
||||||
accessToken: responseData.oauth_token,
|
|
||||||
accessSecret: responseData.oauth_token_secret,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessages = error.response.data.errors
|
|
||||||
.map((error: IJSONObject) => error.message)
|
|
||||||
.join(' ');
|
|
||||||
|
|
||||||
throw new Error(
|
|
||||||
`Error occured while verifying credentials: ${errorMessages}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyCredentials() {
|
async verifyCredentials() {
|
||||||
const verifiedCredentials = await this.client.post(
|
const response = await this.client.verifyAccessToken.run();
|
||||||
`/oauth/access_token?oauth_verifier=${this.connectionData.oauthVerifier}&oauth_token=${this.connectionData.accessToken}`,
|
const responseData = Object.fromEntries(new URLSearchParams(response.data));
|
||||||
null
|
|
||||||
);
|
|
||||||
|
|
||||||
const responseData = Object.fromEntries(
|
|
||||||
new URLSearchParams(verifiedCredentials.data)
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
consumerKey: this.connectionData.consumerKey,
|
consumerKey: this.client.connectionData.consumerKey,
|
||||||
consumerSecret: this.connectionData.consumerSecret,
|
consumerSecret: this.client.connectionData.consumerSecret,
|
||||||
accessToken: responseData.oauth_token,
|
accessToken: responseData.oauth_token,
|
||||||
accessSecret: responseData.oauth_token_secret,
|
accessSecret: responseData.oauth_token_secret,
|
||||||
userId: responseData.user_id,
|
userId: responseData.user_id,
|
||||||
@@ -100,24 +41,7 @@ export default class Authentication implements IAuthentication {
|
|||||||
|
|
||||||
async isStillVerified() {
|
async isStillVerified() {
|
||||||
try {
|
try {
|
||||||
const token = {
|
await this.client.getCurrentUser.run();
|
||||||
key: this.connectionData.accessToken as string,
|
|
||||||
secret: this.connectionData.accessSecret as string,
|
|
||||||
};
|
|
||||||
|
|
||||||
const requestData = {
|
|
||||||
url: `${Authentication.baseUrl}/2/users/me`,
|
|
||||||
method: 'GET',
|
|
||||||
};
|
|
||||||
|
|
||||||
const authHeader = this.oauthClient.toHeader(
|
|
||||||
this.oauthClient.authorize(requestData, token)
|
|
||||||
);
|
|
||||||
|
|
||||||
await this.client.get(`/2/users/me`, {
|
|
||||||
headers: { ...authHeader },
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
|
@@ -0,0 +1,31 @@
|
|||||||
|
import TwitterClient from '../index';
|
||||||
|
|
||||||
|
export default class GetCurrentUser {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
const token = {
|
||||||
|
key: this.client.connectionData.accessToken as string,
|
||||||
|
secret: this.client.connectionData.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)
|
||||||
|
);
|
||||||
|
|
||||||
|
return await this.client.httpClient.get(requestPath, {
|
||||||
|
headers: { ...authHeader },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,44 @@
|
|||||||
|
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.connectionData.accessToken as string,
|
||||||
|
secret: this.client.connectionData.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}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,44 @@
|
|||||||
|
import { IJSONObject } from '@automatisch/types';
|
||||||
|
import TwitterClient from '../index';
|
||||||
|
|
||||||
|
export default class GetUserTweets {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(userId: string) {
|
||||||
|
const token = {
|
||||||
|
key: this.client.connectionData.accessToken as string,
|
||||||
|
secret: this.client.connectionData.accessSecret as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestPath = `/2/users/${userId}/tweets`;
|
||||||
|
|
||||||
|
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}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,42 @@
|
|||||||
|
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}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,20 @@
|
|||||||
|
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.connectionData.oauthVerifier}&oauth_token=${this.client.connectionData.accessToken}`,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(error.response.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
59
packages/backend/src/apps/twitter/client/index.ts
Normal file
59
packages/backend/src/apps/twitter/client/index.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { IJSONObject, IApp } from '@automatisch/types';
|
||||||
|
import OAuth from 'oauth-1.0a';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import HttpClient from '../../../helpers/http-client';
|
||||||
|
import OAuthRequestToken from './endpoints/oauth-request-token';
|
||||||
|
import VerifyAccessToken from './endpoints/verify-access-token';
|
||||||
|
import GetCurrentUser from './endpoints/get-current-user';
|
||||||
|
import GetUserByUsername from './endpoints/get-user-by-username';
|
||||||
|
import GetUserTweets from './endpoints/get-user-tweets';
|
||||||
|
|
||||||
|
export default class TwitterClient {
|
||||||
|
appData: IApp;
|
||||||
|
connectionData: IJSONObject;
|
||||||
|
parameters: IJSONObject;
|
||||||
|
oauthClient: OAuth;
|
||||||
|
httpClient: HttpClient;
|
||||||
|
|
||||||
|
oauthRequestToken: OAuthRequestToken;
|
||||||
|
verifyAccessToken: VerifyAccessToken;
|
||||||
|
getCurrentUser: GetCurrentUser;
|
||||||
|
getUserByUsername: GetUserByUsername;
|
||||||
|
getUserTweets: GetUserTweets;
|
||||||
|
|
||||||
|
static baseUrl = 'https://api.twitter.com';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
appData: IApp,
|
||||||
|
connectionData: IJSONObject,
|
||||||
|
parameters: IJSONObject
|
||||||
|
) {
|
||||||
|
this.connectionData = connectionData;
|
||||||
|
this.appData = appData;
|
||||||
|
this.parameters = parameters;
|
||||||
|
|
||||||
|
this.httpClient = new HttpClient({ baseURL: TwitterClient.baseUrl });
|
||||||
|
|
||||||
|
const consumerData = {
|
||||||
|
key: this.connectionData.consumerKey as string,
|
||||||
|
secret: this.connectionData.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);
|
||||||
|
}
|
||||||
|
}
|
@@ -7,8 +7,11 @@ import {
|
|||||||
import Authentication from './authentication';
|
import Authentication from './authentication';
|
||||||
import Triggers from './triggers';
|
import Triggers from './triggers';
|
||||||
import Actions from './actions';
|
import Actions from './actions';
|
||||||
|
import TwitterClient from './client';
|
||||||
|
|
||||||
export default class Twitter implements IService {
|
export default class Twitter implements IService {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
authenticationClient: IAuthentication;
|
authenticationClient: IAuthentication;
|
||||||
triggers: Triggers;
|
triggers: Triggers;
|
||||||
actions: Actions;
|
actions: Actions;
|
||||||
@@ -18,8 +21,10 @@ export default class Twitter implements IService {
|
|||||||
connectionData: IJSONObject,
|
connectionData: IJSONObject,
|
||||||
parameters: IJSONObject
|
parameters: IJSONObject
|
||||||
) {
|
) {
|
||||||
this.authenticationClient = new Authentication(appData, connectionData);
|
this.client = new TwitterClient(appData, connectionData, parameters);
|
||||||
this.triggers = new Triggers(connectionData, parameters);
|
|
||||||
this.actions = new Actions(connectionData, parameters);
|
this.authenticationClient = new Authentication(this.client);
|
||||||
|
this.triggers = new Triggers(this.client);
|
||||||
|
// this.actions = new Actions(connectionData, parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,12 @@
|
|||||||
import { IJSONObject } from '@automatisch/types';
|
import TwitterClient from './client';
|
||||||
import MyTweet from './triggers/my-tweet';
|
import UserTweet from './triggers/user-tweet';
|
||||||
import SearchTweet from './triggers/search-tweet';
|
|
||||||
|
|
||||||
export default class Triggers {
|
export default class Triggers {
|
||||||
myTweet: MyTweet;
|
client: TwitterClient;
|
||||||
searchTweet: SearchTweet;
|
userTweet: UserTweet;
|
||||||
|
|
||||||
constructor(connectionData: IJSONObject, parameters: IJSONObject) {
|
constructor(client: TwitterClient) {
|
||||||
this.myTweet = new MyTweet(connectionData);
|
this.client = client;
|
||||||
this.searchTweet = new SearchTweet(connectionData, parameters);
|
this.userTweet = new UserTweet(client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
30
packages/backend/src/apps/twitter/triggers/user-tweet.ts
Normal file
30
packages/backend/src/apps/twitter/triggers/user-tweet.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import TwitterClient from '../client';
|
||||||
|
|
||||||
|
export default class UserTweet {
|
||||||
|
client: TwitterClient;
|
||||||
|
|
||||||
|
constructor(client: TwitterClient) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async run() {
|
||||||
|
return this.getTweets();
|
||||||
|
}
|
||||||
|
|
||||||
|
async testRun() {
|
||||||
|
return this.getTweets();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTweets() {
|
||||||
|
const userResponse = await this.client.getUserByUsername.run(
|
||||||
|
this.client.parameters.username as string
|
||||||
|
);
|
||||||
|
|
||||||
|
const userId = userResponse.data.data.id;
|
||||||
|
|
||||||
|
const tweetsResponse = await this.client.getUserTweets.run(userId);
|
||||||
|
const tweets = tweetsResponse.data.data;
|
||||||
|
|
||||||
|
return tweets;
|
||||||
|
}
|
||||||
|
}
|
@@ -30,7 +30,8 @@ const getConnectedApps = async (
|
|||||||
.flat()
|
.flat()
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
const usedApps = [...new Set(duplicatedUsedApps)];
|
const connectionKeys = connections.map((connection) => connection.key);
|
||||||
|
const usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])];
|
||||||
|
|
||||||
apps = apps
|
apps = apps
|
||||||
.filter((app: IApp) => {
|
.filter((app: IApp) => {
|
||||||
|
2
packages/types/index.d.ts
vendored
2
packages/types/index.d.ts
vendored
@@ -171,8 +171,6 @@ export interface ITrigger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IAuthentication {
|
export interface IAuthentication {
|
||||||
appData: IApp;
|
|
||||||
connectionData: IJSONObject;
|
|
||||||
client: unknown;
|
client: unknown;
|
||||||
verifyCredentials(): Promise<IJSONObject>;
|
verifyCredentials(): Promise<IJSONObject>;
|
||||||
isStillVerified(): Promise<boolean>;
|
isStillVerified(): Promise<boolean>;
|
||||||
|
Reference in New Issue
Block a user