diff --git a/packages/backend/src/apps/slack2/auth/verify-credentials.ts b/packages/backend/src/apps/slack2/auth/verify-credentials.ts index 3a58850f..0152348a 100644 --- a/packages/backend/src/apps/slack2/auth/verify-credentials.ts +++ b/packages/backend/src/apps/slack2/auth/verify-credentials.ts @@ -1,7 +1,7 @@ import qs from 'qs'; -import { IGlobalVariableForConnection } from '@automatisch/types'; +import { IGlobalVariable } from '@automatisch/types'; -const verifyCredentials = async ($: IGlobalVariableForConnection) => { +const verifyCredentials = async ($: IGlobalVariable) => { const headers = { 'Content-Type': 'application/x-www-form-urlencoded', }; diff --git a/packages/backend/src/apps/twitter2/auth/create-auth-data.ts b/packages/backend/src/apps/twitter2/auth/create-auth-data.ts index 9c30c8a9..cb37e833 100644 --- a/packages/backend/src/apps/twitter2/auth/create-auth-data.ts +++ b/packages/backend/src/apps/twitter2/auth/create-auth-data.ts @@ -1,12 +1,8 @@ import generateRequest from '../common/generate-request'; -import { - IJSONObject, - IField, - IGlobalVariableForConnection, -} from '@automatisch/types'; +import { IJSONObject, IField, IGlobalVariable } from '@automatisch/types'; import { URLSearchParams } from 'url'; -export default async function createAuthData($: IGlobalVariableForConnection) { +export default async function createAuthData($: IGlobalVariable) { try { const oauthRedirectUrlField = $.app.fields.find( (field: IField) => field.key == 'oAuthRedirectUrl' diff --git a/packages/backend/src/apps/twitter2/auth/is-still-verified.ts b/packages/backend/src/apps/twitter2/auth/is-still-verified.ts index cf2d9a4c..6ecd7ecd 100644 --- a/packages/backend/src/apps/twitter2/auth/is-still-verified.ts +++ b/packages/backend/src/apps/twitter2/auth/is-still-verified.ts @@ -1,7 +1,7 @@ -import { IGlobalVariableForConnection } from '@automatisch/types'; +import { IGlobalVariable } from '@automatisch/types'; import getCurrentUser from '../common/get-current-user'; -const isStillVerified = async ($: IGlobalVariableForConnection) => { +const isStillVerified = async ($: IGlobalVariable) => { try { await getCurrentUser($); return true; diff --git a/packages/backend/src/apps/twitter2/auth/verify-credentials.ts b/packages/backend/src/apps/twitter2/auth/verify-credentials.ts index 332efdbb..4d50b42d 100644 --- a/packages/backend/src/apps/twitter2/auth/verify-credentials.ts +++ b/packages/backend/src/apps/twitter2/auth/verify-credentials.ts @@ -1,7 +1,7 @@ -import { IGlobalVariableForConnection } from '@automatisch/types'; +import { IGlobalVariable } from '@automatisch/types'; import { URLSearchParams } from 'url'; -const verifyCredentials = async ($: IGlobalVariableForConnection) => { +const verifyCredentials = async ($: IGlobalVariable) => { try { const response = await $.http.post( `/oauth/access_token?oauth_verifier=${$.auth.data.oauthVerifier}&oauth_token=${$.auth.data.accessToken}`, diff --git a/packages/backend/src/apps/twitter2/common/generate-request.ts b/packages/backend/src/apps/twitter2/common/generate-request.ts index df6ee2dd..76667636 100644 --- a/packages/backend/src/apps/twitter2/common/generate-request.ts +++ b/packages/backend/src/apps/twitter2/common/generate-request.ts @@ -1,4 +1,4 @@ -import { IGlobalVariableForConnection, IJSONObject } from '@automatisch/types'; +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; import oauthClient from './oauth-client'; import { Token } from 'oauth-1.0a'; @@ -9,7 +9,7 @@ type IGenereateRequestOptons = { }; const generateRequest = async ( - $: IGlobalVariableForConnection, + $: IGlobalVariable, options: IGenereateRequestOptons ) => { const { requestPath, method, data } = options; diff --git a/packages/backend/src/apps/twitter2/common/get-current-user.ts b/packages/backend/src/apps/twitter2/common/get-current-user.ts index 12dfd7b5..0823192f 100644 --- a/packages/backend/src/apps/twitter2/common/get-current-user.ts +++ b/packages/backend/src/apps/twitter2/common/get-current-user.ts @@ -1,7 +1,7 @@ -import { IGlobalVariableForConnection } from '@automatisch/types'; +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; import generateRequest from './generate-request'; -const getCurrentUser = async ($: IGlobalVariableForConnection) => { +const getCurrentUser = async ($: IGlobalVariable): Promise => { const response = await generateRequest($, { requestPath: '/2/users/me', method: 'GET', diff --git a/packages/backend/src/apps/twitter2/common/get-user-by-username.ts b/packages/backend/src/apps/twitter2/common/get-user-by-username.ts index e7e35d16..45e108be 100644 --- a/packages/backend/src/apps/twitter2/common/get-user-by-username.ts +++ b/packages/backend/src/apps/twitter2/common/get-user-by-username.ts @@ -1,10 +1,7 @@ -import { IGlobalVariableForConnection, IJSONObject } from '@automatisch/types'; +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; import generateRequest from './generate-request'; -const getUserByUsername = async ( - $: IGlobalVariableForConnection, - username: string -) => { +const getUserByUsername = async ($: IGlobalVariable, username: string) => { const response = await generateRequest($, { requestPath: `/2/users/by/username/${username}`, method: 'GET', diff --git a/packages/backend/src/apps/twitter2/common/get-user-followers.ts b/packages/backend/src/apps/twitter2/common/get-user-followers.ts new file mode 100644 index 00000000..23e211c0 --- /dev/null +++ b/packages/backend/src/apps/twitter2/common/get-user-followers.ts @@ -0,0 +1,59 @@ +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; +import { URLSearchParams } from 'url'; +import { omitBy, isEmpty } from 'lodash'; +import generateRequest from './generate-request'; + +type GetUserFollowersOptions = { + userId: string; + lastInternalId?: string; +}; + +const getUserFollowers = async ( + $: IGlobalVariable, + options: GetUserFollowersOptions +) => { + let response; + const followers: IJSONObject[] = []; + + do { + const params: IJSONObject = { + pagination_token: response?.data?.meta?.next_token, + }; + + const queryParams = new URLSearchParams(omitBy(params, isEmpty)); + + const requestPath = `/2/users/${options.userId}/followers${ + queryParams.toString() ? `?${queryParams.toString()}` : '' + }`; + + response = await generateRequest($, { + requestPath, + method: 'GET', + }); + + if (response.data.meta.result_count > 0) { + response.data.data.forEach((tweet: IJSONObject) => { + if ( + !options.lastInternalId || + Number(tweet.id) > Number(options.lastInternalId) + ) { + followers.push(tweet); + } else { + return; + } + }); + } + } while (response.data.meta.next_token && options.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; +}; + +export default getUserFollowers; diff --git a/packages/backend/src/apps/twitter2/common/get-user-tweets.ts b/packages/backend/src/apps/twitter2/common/get-user-tweets.ts index 219820e7..39889d9b 100644 --- a/packages/backend/src/apps/twitter2/common/get-user-tweets.ts +++ b/packages/backend/src/apps/twitter2/common/get-user-tweets.ts @@ -1,26 +1,44 @@ -import { IGlobalVariableForConnection, IJSONObject } from '@automatisch/types'; +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; import { URLSearchParams } from 'url'; import omitBy from 'lodash/omitBy'; import isEmpty from 'lodash/isEmpty'; import generateRequest from './generate-request'; +import getCurrentUser from './get-current-user'; +import getUserByUsername from './get-user-by-username'; + +type IGetUserTweetsOptions = { + currentUser: boolean; + userId?: string; + lastInternalId?: string; +}; const getUserTweets = async ( - $: IGlobalVariableForConnection, - userId: string, - lastInternalId?: string + $: IGlobalVariable, + options: IGetUserTweetsOptions ) => { + let username: string; + + if (options.currentUser) { + const currentUser = await getCurrentUser($); + username = currentUser.username as string; + } else { + username = $.db.step.parameters.username as string; + } + + const user = await getUserByUsername($, username); + let response; const tweets: IJSONObject[] = []; do { const params: IJSONObject = { - since_id: lastInternalId, + since_id: options.lastInternalId, pagination_token: response?.data?.meta?.next_token, }; const queryParams = new URLSearchParams(omitBy(params, isEmpty)); - const requestPath = `/2/users/${userId}/tweets${ + const requestPath = `/2/users/${user.id}/tweets${ queryParams.toString() ? `?${queryParams.toString()}` : '' }`; @@ -31,14 +49,17 @@ const getUserTweets = async ( if (response.data.meta.result_count > 0) { response.data.data.forEach((tweet: IJSONObject) => { - if (!lastInternalId || Number(tweet.id) > Number(lastInternalId)) { + if ( + !options.lastInternalId || + Number(tweet.id) > Number(options.lastInternalId) + ) { tweets.push(tweet); } else { return; } }); } - } while (response.data.meta.next_token && lastInternalId); + } while (response.data.meta.next_token && options.lastInternalId); if (response.data.errors) { const errorMessages = response.data.errors diff --git a/packages/backend/src/apps/twitter2/common/oauth-client.ts b/packages/backend/src/apps/twitter2/common/oauth-client.ts index d4a59b2e..c29f6a0a 100644 --- a/packages/backend/src/apps/twitter2/common/oauth-client.ts +++ b/packages/backend/src/apps/twitter2/common/oauth-client.ts @@ -1,8 +1,8 @@ -import { IGlobalVariableForConnection } from '@automatisch/types'; +import { IGlobalVariable } from '@automatisch/types'; import crypto from 'crypto'; import OAuth from 'oauth-1.0a'; -const oauthClient = ($: IGlobalVariableForConnection) => { +const oauthClient = ($: IGlobalVariable) => { const consumerData = { key: $.auth.data.consumerKey as string, secret: $.auth.data.consumerSecret as string, diff --git a/packages/backend/src/apps/twitter2/triggers/my-tweets/index.ts b/packages/backend/src/apps/twitter2/triggers/my-tweets/index.ts index 33ad3c4f..e6cb95a0 100644 --- a/packages/backend/src/apps/twitter2/triggers/my-tweets/index.ts +++ b/packages/backend/src/apps/twitter2/triggers/my-tweets/index.ts @@ -1,6 +1,4 @@ import { IGlobalVariable } from '@automatisch/types'; -import getCurrentUser from '../../common/get-current-user'; -import getUserByUsername from '../../common/get-user-by-username'; import getUserTweets from '../../common/get-user-tweets'; export default { @@ -20,18 +18,13 @@ export default { ], async run($: IGlobalVariable) { - return this.getTweets($, await $.db.flow.lastInternalId()); + return await getUserTweets($, { + currentUser: true, + lastInternalId: $.db.flow.lastInternalId, + }); }, async testRun($: IGlobalVariable) { - return this.getTweets($); - }, - - async getTweets($: IGlobalVariable, lastInternalId?: string) { - const { username } = await getCurrentUser($); - const user = await getUserByUsername($, username); - - const tweets = await getUserTweets($, user.id, lastInternalId); - return tweets; + return await getUserTweets($, { currentUser: true }); }, }; diff --git a/packages/backend/src/apps/twitter2/triggers/new-follower-of-me/index.ts b/packages/backend/src/apps/twitter2/triggers/new-follower-of-me/index.ts new file mode 100644 index 00000000..c9b4be84 --- /dev/null +++ b/packages/backend/src/apps/twitter2/triggers/new-follower-of-me/index.ts @@ -0,0 +1,27 @@ +import { IGlobalVariable } from '@automatisch/types'; +import myFollowers from './my-followers'; + +export default { + name: 'New follower of me', + key: 'myFollowers', + pollInterval: 15, + description: 'Will be triggered when you have a new follower.', + substeps: [ + { + key: 'chooseConnection', + name: 'Choose connection', + }, + { + key: 'testStep', + name: 'Test trigger', + }, + ], + + async run($: IGlobalVariable) { + return await myFollowers($, $.db.flow.lastInternalId); + }, + + async testRun($: IGlobalVariable) { + return await myFollowers($); + }, +}; diff --git a/packages/backend/src/apps/twitter2/triggers/new-follower-of-me/my-followers.ts b/packages/backend/src/apps/twitter2/triggers/new-follower-of-me/my-followers.ts new file mode 100644 index 00000000..11665a5a --- /dev/null +++ b/packages/backend/src/apps/twitter2/triggers/new-follower-of-me/my-followers.ts @@ -0,0 +1,17 @@ +import { IGlobalVariable } from "@automatisch/types"; +import getCurrentUser from "../../common/get-current-user"; +import getUserByUsername from "../../common/get-user-by-username"; +import getUserFollowers from "../../common/get-user-followers"; + +const myFollowers = async( $: IGlobalVariable, lastInternalId?: string) => { + const { username } = await getCurrentUser($); + const user = await getUserByUsername($, username as string); + + const tweets = await getUserFollowers($, { + userId: user.id, + lastInternalId + }); + return tweets; +}); + +export default myFollowers; diff --git a/packages/backend/src/apps/twitter2/triggers/search-tweets/index.ts b/packages/backend/src/apps/twitter2/triggers/search-tweets/index.ts new file mode 100644 index 00000000..0f67d908 --- /dev/null +++ b/packages/backend/src/apps/twitter2/triggers/search-tweets/index.ts @@ -0,0 +1,45 @@ +import { IGlobalVariable } from '@automatisch/types'; +import searchTweets from './search-tweets'; + +export default { + name: 'Search Tweets', + key: 'searchTweets', + pollInterval: 15, + description: + 'Will be triggered when any user tweet something containing a specific keyword, phrase, username or hashtag.', + substeps: [ + { + key: 'chooseConnection', + name: 'Choose connection', + }, + { + key: 'chooseTrigger', + name: 'Set up a trigger', + arguments: [ + { + label: 'Search Term', + key: 'searchTerm', + type: 'string', + required: true, + }, + ], + }, + { + key: 'testStep', + name: 'Test trigger', + }, + ], + + async run($: IGlobalVariable) { + return await searchTweets($, { + searchTerm: $.db.step.parameters.searchTerm as string, + lastInternalId: $.db.flow.lastInternalId, + }); + }, + + async testRun($: IGlobalVariable) { + return await searchTweets($, { + searchTerm: $.db.step.parameters.searchTerm as string, + }); + }, +}; diff --git a/packages/backend/src/apps/twitter2/triggers/search-tweets/search-tweets.ts b/packages/backend/src/apps/twitter2/triggers/search-tweets/search-tweets.ts new file mode 100644 index 00000000..df7bbe21 --- /dev/null +++ b/packages/backend/src/apps/twitter2/triggers/search-tweets/search-tweets.ts @@ -0,0 +1,65 @@ +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; +import qs from 'qs'; +import generateRequest from '../../common/generate-request'; +import { omitBy, isEmpty } from 'lodash'; + +type ISearchTweetsOptions = { + searchTerm: string; + lastInternalId?: string; +}; + +const searchTweets = async ( + $: IGlobalVariable, + options: ISearchTweetsOptions +) => { + let response; + + const tweets: { + data: IJSONObject[]; + error: IJSONObject | null; + } = { + data: [], + error: null, + }; + + do { + const params: IJSONObject = { + query: options.searchTerm, + since_id: options.lastInternalId, + pagination_token: response?.data?.meta?.next_token, + }; + + const queryParams = qs.stringify(omitBy(params, isEmpty)); + + const requestPath = `/2/tweets/search/recent${ + queryParams.toString() ? `?${queryParams.toString()}` : '' + }`; + + response = await generateRequest($, { + requestPath, + method: 'GET', + }); + + if (response.integrationError) { + tweets.error = response.integrationError; + return tweets; + } + + if (response.data.meta.result_count > 0) { + response.data.data.forEach((tweet: IJSONObject) => { + if ( + !options.lastInternalId || + Number(tweet.id) > Number(options.lastInternalId) + ) { + tweets.data.push(tweet); + } else { + return; + } + }); + } + } while (response.data.meta.next_token && options.lastInternalId); + + return tweets; +}; + +export default searchTweets; diff --git a/packages/backend/src/apps/twitter2/triggers/user-tweets/index.ts b/packages/backend/src/apps/twitter2/triggers/user-tweets/index.ts new file mode 100644 index 00000000..23517c9e --- /dev/null +++ b/packages/backend/src/apps/twitter2/triggers/user-tweets/index.ts @@ -0,0 +1,46 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getUserTweets from '../../common/get-user-tweets'; + +export default { + name: 'User Tweets', + key: 'userTweets', + pollInterval: 15, + description: 'Will be triggered when a specific user tweet something new.', + substeps: [ + { + key: 'chooseConnection', + name: 'Choose connection', + }, + { + key: 'chooseTrigger', + name: 'Set up a trigger', + arguments: [ + { + label: 'Username', + key: 'username', + type: 'string', + required: true, + }, + ], + }, + { + key: 'testStep', + name: 'Test trigger', + }, + ], + + async run($: IGlobalVariable) { + return await getUserTweets($, { + currentUser: false, + userId: $.db.step.parameters.username as string, + lastInternalId: $.db.flow.lastInternalId, + }); + }, + + async testRun($: IGlobalVariable) { + return await getUserTweets($, { + currentUser: false, + userId: $.db.step.parameters.username as string, + }); + }, +}; diff --git a/packages/backend/src/helpers/global-variable.ts b/packages/backend/src/helpers/global-variable.ts index 54ea59c7..e064a8cb 100644 --- a/packages/backend/src/helpers/global-variable.ts +++ b/packages/backend/src/helpers/global-variable.ts @@ -1,29 +1,40 @@ import createHttpClient from './http-client'; import Connection from '../models/connection'; import Flow from '../models/flow'; +import Step from '../models/step'; import { IJSONObject, IApp, IGlobalVariable } from '@automatisch/types'; -const globalVariable = ( +const globalVariable = async ( connection: Connection, appData: IApp, - flow?: Flow -): IGlobalVariable => { + flow?: Flow, + currentStep?: Step +): Promise => { + const lastInternalId = await flow?.lastInternalId(); + return { auth: { set: async (args: IJSONObject) => { - return await connection.$query().patchAndFetch({ + await connection.$query().patchAndFetch({ formattedData: { ...connection.formattedData, ...args, }, }); + + return null; }, data: connection.formattedData, }, app: appData, http: createHttpClient({ baseURL: appData.baseUrl }), db: { - flow: flow, + flow: { + lastInternalId, + }, + step: { + parameters: currentStep?.parameters || {}, + }, }, }; }; diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 495b244e..af658d4f 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -197,13 +197,18 @@ export type IHttpClientParams = { export type IGlobalVariable = { auth: { - set: (args: IJSONObject) => Promise; + set: (args: IJSONObject) => Promise; data: IJSONObject; }; app: IApp; http: IHttpClient; db: { - flow: IFlow; + flow: { + lastInternalId: string; + }; + step: { + parameters: IJSONObject; + } }; };