From e97c7e2e68ae0bf3276f5d7fcdcf7571072bd2a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=B1dvan=20Akca?= Date: Wed, 1 Nov 2023 18:13:52 +0300 Subject: [PATCH] feat(twitch): add twitch integration --- .../apps/spotify/common/add-auth-header.ts | 3 +- .../src/apps/twitch/assets/favicon.svg | 1 + .../src/apps/twitch/auth/generate-auth-url.ts | 22 +++++++ .../backend/src/apps/twitch/auth/index.ts | 48 ++++++++++++++ .../src/apps/twitch/auth/is-still-verified.ts | 8 +++ .../src/apps/twitch/auth/refresh-token.ts | 27 ++++++++ .../apps/twitch/auth/verify-credentials.ts | 63 +++++++++++++++++++ .../src/apps/twitch/common/add-auth-header.ts | 20 ++++++ .../src/apps/twitch/common/auth-scope.ts | 3 + .../apps/twitch/common/get-current-user.ts | 8 +++ packages/backend/src/apps/twitch/index.d.ts | 0 packages/backend/src/apps/twitch/index.ts | 16 +++++ packages/docs/pages/.vitepress/config.js | 6 ++ packages/docs/pages/apps/twitch/connection.md | 19 ++++++ .../docs/pages/public/favicons/twitch.svg | 1 + 15 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/apps/twitch/assets/favicon.svg create mode 100644 packages/backend/src/apps/twitch/auth/generate-auth-url.ts create mode 100644 packages/backend/src/apps/twitch/auth/index.ts create mode 100644 packages/backend/src/apps/twitch/auth/is-still-verified.ts create mode 100644 packages/backend/src/apps/twitch/auth/refresh-token.ts create mode 100644 packages/backend/src/apps/twitch/auth/verify-credentials.ts create mode 100644 packages/backend/src/apps/twitch/common/add-auth-header.ts create mode 100644 packages/backend/src/apps/twitch/common/auth-scope.ts create mode 100644 packages/backend/src/apps/twitch/common/get-current-user.ts create mode 100644 packages/backend/src/apps/twitch/index.d.ts create mode 100644 packages/backend/src/apps/twitch/index.ts create mode 100644 packages/docs/pages/apps/twitch/connection.md create mode 100644 packages/docs/pages/public/favicons/twitch.svg diff --git a/packages/backend/src/apps/spotify/common/add-auth-header.ts b/packages/backend/src/apps/spotify/common/add-auth-header.ts index d16f394f..1ae95da8 100644 --- a/packages/backend/src/apps/spotify/common/add-auth-header.ts +++ b/packages/backend/src/apps/spotify/common/add-auth-header.ts @@ -1,7 +1,8 @@ import { TBeforeRequest } from '@automatisch/types'; const addAuthHeader: TBeforeRequest = ($, requestConfig) => { - if (requestConfig.additionalProperties?.skipAddingAuthHeader) return requestConfig; + if (requestConfig.additionalProperties?.skipAddingAuthHeader) + return requestConfig; if ($.auth.data?.accessToken) { const authorizationHeader = `Bearer ${$.auth.data.accessToken}`; diff --git a/packages/backend/src/apps/twitch/assets/favicon.svg b/packages/backend/src/apps/twitch/assets/favicon.svg new file mode 100644 index 00000000..4e162ffa --- /dev/null +++ b/packages/backend/src/apps/twitch/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/twitch/auth/generate-auth-url.ts b/packages/backend/src/apps/twitch/auth/generate-auth-url.ts new file mode 100644 index 00000000..8ed1996c --- /dev/null +++ b/packages/backend/src/apps/twitch/auth/generate-auth-url.ts @@ -0,0 +1,22 @@ +import { IField, IGlobalVariable } from '@automatisch/types'; +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope'; + +export default async function generateAuthUrl($: IGlobalVariable) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value as string; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId as string, + redirect_uri: redirectUri, + response_type: 'code', + scope: authScope.join(' '), + }); + + const url = `https://id.twitch.tv/oauth2/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/twitch/auth/index.ts b/packages/backend/src/apps/twitch/auth/index.ts new file mode 100644 index 00000000..0391ee6d --- /dev/null +++ b/packages/backend/src/apps/twitch/auth/index.ts @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url'; +import verifyCredentials from './verify-credentials'; +import refreshToken from './refresh-token'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string' as const, + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/twitch/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Twitch, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/twitch/auth/is-still-verified.ts b/packages/backend/src/apps/twitch/auth/is-still-verified.ts new file mode 100644 index 00000000..e219ba7c --- /dev/null +++ b/packages/backend/src/apps/twitch/auth/is-still-verified.ts @@ -0,0 +1,8 @@ +import { IGlobalVariable } from '@automatisch/types'; + +const isStillVerified = async ($: IGlobalVariable) => { + const { data } = await $.http.get('https://id.twitch.tv/oauth2/validate'); + return !!data.login; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/twitch/auth/refresh-token.ts b/packages/backend/src/apps/twitch/auth/refresh-token.ts new file mode 100644 index 00000000..7cb60073 --- /dev/null +++ b/packages/backend/src/apps/twitch/auth/refresh-token.ts @@ -0,0 +1,27 @@ +import { URLSearchParams } from 'node:url'; +import { IGlobalVariable } from '@automatisch/types'; +import authScope from '../common/auth-scope'; + +const refreshToken = async ($: IGlobalVariable) => { + const params = new URLSearchParams({ + client_id: $.auth.data.clientId as string, + client_secret: $.auth.data.clientSecret as string, + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken as string, + }); + + const { data } = await $.http.post( + 'https://id.twitch.tv/oauth2/token', + params.toString() + ); + + await $.auth.set({ + accessToken: data.access_token, + refreshToken: data.refresh_token, + expiresIn: data.expires_in, + scope: authScope.join(' '), + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/twitch/auth/verify-credentials.ts b/packages/backend/src/apps/twitch/auth/verify-credentials.ts new file mode 100644 index 00000000..323b89d3 --- /dev/null +++ b/packages/backend/src/apps/twitch/auth/verify-credentials.ts @@ -0,0 +1,63 @@ +import { IField, IGlobalVariable } from '@automatisch/types'; +import getCurrentUser from '../common/get-current-user'; + +const verifyCredentials = async ($: IGlobalVariable) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value as string; + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', + }; + const userParams = { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + redirect_uri: redirectUri, + }; + + const { data } = await $.http.post( + `https://id.twitch.tv/oauth2/token`, + null, + { headers, params: userParams } + ); + + await $.auth.set({ + userAccessToken: data.access_token, + }); + + const currentUser = await getCurrentUser($); + + const screenName = [currentUser.display_name, currentUser.email] + .filter(Boolean) + .join(' @ '); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + scope: $.auth.data.scope, + userExpiresIn: data.expires_in, + userRefreshToken: data.refresh_token, + screenName, + }); + + const appParams = { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + grant_type: 'client_credentials', + }; + + const response = await $.http.post( + `https://id.twitch.tv/oauth2/token`, + null, + { headers, params: appParams } + ); + + await $.auth.set({ + appAccessToken: response.data.access_token, + appExpiresIn: response.data.expires_in, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/twitch/common/add-auth-header.ts b/packages/backend/src/apps/twitch/common/add-auth-header.ts new file mode 100644 index 00000000..a0f09eee --- /dev/null +++ b/packages/backend/src/apps/twitch/common/add-auth-header.ts @@ -0,0 +1,20 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + const clientId = $.auth.data.clientId as string; + let token; + if (requestConfig.additionalProperties?.appAccessToken) { + token = $.auth.data.appAccessToken; + } else { + token = $.auth.data.userAccessToken; + } + + if (token && clientId) { + requestConfig.headers.Authorization = `Bearer ${token}`; + requestConfig.headers['Client-Id'] = clientId; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/twitch/common/auth-scope.ts b/packages/backend/src/apps/twitch/common/auth-scope.ts new file mode 100644 index 00000000..afe342db --- /dev/null +++ b/packages/backend/src/apps/twitch/common/auth-scope.ts @@ -0,0 +1,3 @@ +const authScope: string[] = ['user:read:email']; + +export default authScope; diff --git a/packages/backend/src/apps/twitch/common/get-current-user.ts b/packages/backend/src/apps/twitch/common/get-current-user.ts new file mode 100644 index 00000000..8a461c14 --- /dev/null +++ b/packages/backend/src/apps/twitch/common/get-current-user.ts @@ -0,0 +1,8 @@ +import { IGlobalVariable } from '@automatisch/types'; + +const getCurrentUser = async ($: IGlobalVariable) => { + const { data: currentUser } = await $.http.get('/helix/users'); + return currentUser.data[0]; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/twitch/index.d.ts b/packages/backend/src/apps/twitch/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/twitch/index.ts b/packages/backend/src/apps/twitch/index.ts new file mode 100644 index 00000000..1e123555 --- /dev/null +++ b/packages/backend/src/apps/twitch/index.ts @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import auth from './auth'; + +export default defineApp({ + name: 'Twitch', + key: 'twitch', + baseUrl: 'https://www.twitch.tv', + apiBaseUrl: 'https://api.twitch.tv', + iconUrl: '{BASE_URL}/apps/twitch/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/twitch/connection', + primaryColor: '5C16C5', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, +}); diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js index 15e92517..c36b0199 100644 --- a/packages/docs/pages/.vitepress/config.js +++ b/packages/docs/pages/.vitepress/config.js @@ -392,6 +392,12 @@ export default defineConfig({ { text: 'Connection', link: '/apps/twilio/connection' }, ], }, + { + text: 'Twitch', + collapsible: true, + collapsed: true, + items: [{ text: 'Connection', link: '/apps/twitch/connection' }], + }, { text: 'Twitter', collapsible: true, diff --git a/packages/docs/pages/apps/twitch/connection.md b/packages/docs/pages/apps/twitch/connection.md new file mode 100644 index 00000000..4320c483 --- /dev/null +++ b/packages/docs/pages/apps/twitch/connection.md @@ -0,0 +1,19 @@ +# Twitch + +:::info +This page explains the steps you need to follow to set up the Twitch +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [developer console](https://dev.twitch.tv/console) to register an app. +2. Select on the **Applications** tab and click on the **Register Your Application**. +3. Enter a name for your app. +4. Copy **OAuth Redirect URL** from Automatisch to **OAuth Redirect URLs** field. +5. Select a **Category** and click on the **Create** button. +6. Go back to **Applications** tab and choose your app under **Developer Applications**. +7. Click the **Manage**. +8. Copy the **Your Client ID** value from the following popup to the `Client ID` field on Automatisch. +9. Click on the **New Secret** and generate your client secret key. +10. Copy the **Your Client Secret** value from the following popup to the `Client Secret` field on Automatisch. +11. Click **Submit** button on Automatisch. +12. Congrats! Start using your new Twitch connection within the flows. diff --git a/packages/docs/pages/public/favicons/twitch.svg b/packages/docs/pages/public/favicons/twitch.svg new file mode 100644 index 00000000..4e162ffa --- /dev/null +++ b/packages/docs/pages/public/favicons/twitch.svg @@ -0,0 +1 @@ + \ No newline at end of file