diff --git a/packages/backend/src/apps/twitter/authentication.ts b/packages/backend/src/apps/twitter/authentication.ts index 24674737..5beb7fb4 100644 --- a/packages/backend/src/apps/twitter/authentication.ts +++ b/packages/backend/src/apps/twitter/authentication.ts @@ -1,96 +1,37 @@ -import type { - IAuthentication, - IApp, - IField, - IJSONObject, -} from '@automatisch/types'; -import HttpClient from '../../helpers/http-client'; -import OAuth from 'oauth-1.0a'; -import crypto from 'crypto'; +import type { IAuthentication, IField } from '@automatisch/types'; import { URLSearchParams } from 'url'; +import TwitterClient from './client'; export default class Authentication implements IAuthentication { - appData: IApp; - connectionData: IJSONObject; + client: TwitterClient; - client: HttpClient; - oauthClient: OAuth; - - 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'); - }, - }); + constructor(client: TwitterClient) { + this.client = client; } async createAuthData() { - const appFields = this.appData.fields.find( + const appFields = this.client.appData.fields.find( (field: IField) => field.key == 'oAuthRedirectUrl' ); const callbackUrl = appFields.value; - const requestData = { - url: `${Authentication.baseUrl}/oauth/request_token`, - method: 'POST', - data: { oauth_callback: callbackUrl }, + 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, }; - - 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() { - const verifiedCredentials = await this.client.post( - `/oauth/access_token?oauth_verifier=${this.connectionData.oauthVerifier}&oauth_token=${this.connectionData.accessToken}`, - null - ); - - const responseData = Object.fromEntries( - new URLSearchParams(verifiedCredentials.data) - ); + const response = await this.client.verifyAccessToken.run(); + const responseData = Object.fromEntries(new URLSearchParams(response.data)); return { - consumerKey: this.connectionData.consumerKey, - consumerSecret: this.connectionData.consumerSecret, + consumerKey: this.client.connectionData.consumerKey, + consumerSecret: this.client.connectionData.consumerSecret, accessToken: responseData.oauth_token, accessSecret: responseData.oauth_token_secret, userId: responseData.user_id, @@ -100,24 +41,7 @@ export default class Authentication implements IAuthentication { async isStillVerified() { try { - const token = { - 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 }, - }); - + await this.client.getCurrentUser.run(); return true; } catch (error) { return false; diff --git a/packages/backend/src/apps/twitter/client/endpoints/get-current-user.ts b/packages/backend/src/apps/twitter/client/endpoints/get-current-user.ts new file mode 100644 index 00000000..893a8fa5 --- /dev/null +++ b/packages/backend/src/apps/twitter/client/endpoints/get-current-user.ts @@ -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 }, + }); + } +} diff --git a/packages/backend/src/apps/twitter/client/endpoints/get-user-by-username.ts b/packages/backend/src/apps/twitter/client/endpoints/get-user-by-username.ts new file mode 100644 index 00000000..173699c1 --- /dev/null +++ b/packages/backend/src/apps/twitter/client/endpoints/get-user-by-username.ts @@ -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; + } +} diff --git a/packages/backend/src/apps/twitter/client/endpoints/get-user-tweets.ts b/packages/backend/src/apps/twitter/client/endpoints/get-user-tweets.ts new file mode 100644 index 00000000..da2bf3cc --- /dev/null +++ b/packages/backend/src/apps/twitter/client/endpoints/get-user-tweets.ts @@ -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; + } +} diff --git a/packages/backend/src/apps/twitter/client/endpoints/oauth-request-token.ts b/packages/backend/src/apps/twitter/client/endpoints/oauth-request-token.ts new file mode 100644 index 00000000..6ef9d899 --- /dev/null +++ b/packages/backend/src/apps/twitter/client/endpoints/oauth-request-token.ts @@ -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}` + ); + } + } +} diff --git a/packages/backend/src/apps/twitter/client/endpoints/verify-access-token.ts b/packages/backend/src/apps/twitter/client/endpoints/verify-access-token.ts new file mode 100644 index 00000000..2bd7e470 --- /dev/null +++ b/packages/backend/src/apps/twitter/client/endpoints/verify-access-token.ts @@ -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); + } + } +} diff --git a/packages/backend/src/apps/twitter/client/index.ts b/packages/backend/src/apps/twitter/client/index.ts new file mode 100644 index 00000000..d06d9c95 --- /dev/null +++ b/packages/backend/src/apps/twitter/client/index.ts @@ -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); + } +} diff --git a/packages/backend/src/apps/twitter/index.ts b/packages/backend/src/apps/twitter/index.ts index 00267773..2c0fc8a8 100644 --- a/packages/backend/src/apps/twitter/index.ts +++ b/packages/backend/src/apps/twitter/index.ts @@ -7,8 +7,11 @@ import { 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; @@ -18,8 +21,10 @@ export default class Twitter implements IService { connectionData: IJSONObject, parameters: IJSONObject ) { - this.authenticationClient = new Authentication(appData, connectionData); - this.triggers = new Triggers(connectionData, parameters); - this.actions = new Actions(connectionData, parameters); + this.client = new TwitterClient(appData, connectionData, parameters); + + this.authenticationClient = new Authentication(this.client); + this.triggers = new Triggers(this.client); + // this.actions = new Actions(connectionData, parameters); } } diff --git a/packages/backend/src/apps/twitter/triggers.ts b/packages/backend/src/apps/twitter/triggers.ts index 9f3cf4c2..40ea39bd 100644 --- a/packages/backend/src/apps/twitter/triggers.ts +++ b/packages/backend/src/apps/twitter/triggers.ts @@ -1,13 +1,12 @@ -import { IJSONObject } from '@automatisch/types'; -import MyTweet from './triggers/my-tweet'; -import SearchTweet from './triggers/search-tweet'; +import TwitterClient from './client'; +import UserTweet from './triggers/user-tweet'; export default class Triggers { - myTweet: MyTweet; - searchTweet: SearchTweet; + client: TwitterClient; + userTweet: UserTweet; - constructor(connectionData: IJSONObject, parameters: IJSONObject) { - this.myTweet = new MyTweet(connectionData); - this.searchTweet = new SearchTweet(connectionData, parameters); + constructor(client: TwitterClient) { + this.client = client; + this.userTweet = new UserTweet(client); } } diff --git a/packages/backend/src/apps/twitter/triggers/user-tweet.ts b/packages/backend/src/apps/twitter/triggers/user-tweet.ts new file mode 100644 index 00000000..a4d42b94 --- /dev/null +++ b/packages/backend/src/apps/twitter/triggers/user-tweet.ts @@ -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; + } +} diff --git a/packages/backend/src/graphql/queries/get-connected-apps.ts b/packages/backend/src/graphql/queries/get-connected-apps.ts index 31780691..ecf18847 100644 --- a/packages/backend/src/graphql/queries/get-connected-apps.ts +++ b/packages/backend/src/graphql/queries/get-connected-apps.ts @@ -30,7 +30,8 @@ const getConnectedApps = async ( .flat() .filter(Boolean); - const usedApps = [...new Set(duplicatedUsedApps)]; + const connectionKeys = connections.map((connection) => connection.key); + const usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])]; apps = apps .filter((app: IApp) => { diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 2ec94cfa..b4f39bd7 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -171,8 +171,6 @@ export interface ITrigger { } export interface IAuthentication { - appData: IApp; - connectionData: IJSONObject; client: unknown; verifyCredentials(): Promise; isStillVerified(): Promise;