Merge pull request #673 from automatisch/issue-495

feat(slack): add bot capability in send message action
This commit is contained in:
Ömer Faruk Aydın
2022-11-03 18:04:25 +01:00
committed by GitHub
11 changed files with 359 additions and 56 deletions

View File

@@ -32,13 +32,46 @@ export default defineAction({
description: 'The content of your new message.',
variables: true,
},
{
label: 'Send as a bot?',
key: 'sendAsBot',
type: 'dropdown' as const,
required: false,
value: false,
description: 'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.',
variables: false,
options: [
{
label: 'Yes',
value: true,
},
{
label: 'No',
value: false,
}
]
},
{
label: 'Bot name',
key: 'botName',
type: 'string' as const,
required: true,
value: 'Automatisch',
description: 'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.',
variables: true,
},
{
label: 'Bot icon',
key: 'botIcon',
type: 'string' as const,
required: false,
description: 'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:',
variables: true,
},
],
async run($) {
const channelId = $.step.parameters.channel as string;
const text = $.step.parameters.message as string;
const message = await postMessage($, channelId, text);
const message = await postMessage($);
return message;
},

View File

@@ -1,23 +1,54 @@
import { IGlobalVariable } from '@automatisch/types';
import { URL } from 'url';
const postMessage = async (
$: IGlobalVariable,
channelId: string,
text: string
) => {
const params = {
type TData = {
channel: string;
text: string;
username?: string;
icon_url?: string;
icon_emoji?: string;
}
const postMessage = async ($: IGlobalVariable) => {
const { parameters } = $.step;
const channelId = parameters.channel as string;
const text = parameters.message as string;
const sendAsBot = parameters.sendAsBot as boolean;
const botName = parameters.botName as string;
const botIcon = parameters.botIcon as string;
const data: TData = {
channel: channelId,
text,
};
const response = await $.http.post('/chat.postMessage', params);
if (sendAsBot) {
data.username = botName;
try {
// challenging the input to check if it is a URL!
new URL(botIcon);
data.icon_url = botIcon;
} catch {
data.icon_emoji = botIcon;
}
}
const customConfig = {
sendAsBot,
};
const response = await $.http.post(
'/chat.postMessage',
data,
{ additionalProperties: customConfig },
);
if (response.data.ok === false) {
throw new Error(JSON.stringify(response.data));
}
const message = {
raw: response?.data?.message,
raw: response?.data,
};
$.setActionItem(message);

View File

@@ -0,0 +1,62 @@
import { IField, IGlobalVariable } from '@automatisch/types';
import qs from 'qs';
const scopes = [
'channels:manage',
'channels:read',
'channels:join',
'chat:write',
'chat:write.customize',
'chat:write.public',
'files:write',
'im:write',
'mpim:write',
'team:read',
'users.profile:read',
'users:read',
'workflow.steps:execute',
'users:read.email',
'commands',
];
const userScopes = [
'channels:history',
'channels:read',
'channels:write',
'chat:write',
'emoji:read',
'files:read',
'files:write',
'groups:history',
'groups:read',
'groups:write',
'im:write',
'mpim:write',
'reactions:read',
'reminders:write',
'search:read',
'stars:read',
'team:read',
'users.profile:read',
'users.profile:write',
'users:read',
'users:read.email',
];
export default async function createAuthData($: IGlobalVariable) {
const oauthRedirectUrlField = $.app.auth.fields.find(
(field: IField) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value as string;
const searchParams = qs.stringify({
client_id: $.auth.data.consumerKey as string,
redirect_uri: redirectUri,
scope: scopes.join(','),
user_scope: userScopes.join(','),
});
const url = `${$.app.baseUrl}/oauth/v2/authorize?${searchParams}`;
await $.auth.set({
url,
});
};

View File

@@ -1,17 +1,41 @@
import createAuthData from './create-auth-data';
import verifyCredentials from './verify-credentials';
import isStillVerified from './is-still-verified';
export default {
fields: [
{
key: 'accessToken',
label: 'Access Token',
key: 'oAuthRedirectUrl',
label: 'OAuth Redirect URL',
type: 'string' as const,
required: true,
readOnly: true,
value: '{WEB_APP_URL}/app/slack/connections/add',
placeholder: null,
description:
'When asked to input an OAuth callback or redirect URL in Slack OAuth, enter the URL above.',
clickToCopy: true,
},
{
key: 'consumerKey',
label: 'API Key',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description: 'Access token of slack that Automatisch will connect to.',
description: null,
clickToCopy: false,
},
{
key: 'consumerSecret',
label: 'API Secret',
type: 'string' as const,
required: true,
readOnly: false,
value: null,
placeholder: null,
description: null,
clickToCopy: false,
},
],
@@ -30,8 +54,12 @@ export default {
value: null,
properties: [
{
name: 'accessToken',
value: '{fields.accessToken}',
name: 'consumerKey',
value: '{fields.consumerKey}',
},
{
name: 'consumerSecret',
value: '{fields.consumerSecret}',
},
],
},
@@ -40,6 +68,53 @@ export default {
{
step: 2,
type: 'mutation' as const,
name: 'createAuthData',
arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
],
},
{
step: 3,
type: 'openWithPopup' as const,
name: 'openAuthPopup',
arguments: [
{
name: 'url',
value: '{createAuthData.url}',
},
],
},
{
step: 4,
type: 'mutation' as const,
name: 'updateConnection',
arguments: [
{
name: 'id',
value: '{createConnection.id}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'code',
value: '{openAuthPopup.code}',
},
{
name: 'state',
value: '{openAuthPopup.state}',
},
],
},
],
},
{
step: 5,
type: 'mutation' as const,
name: 'verifyConnection',
arguments: [
{
@@ -75,8 +150,12 @@ export default {
value: null,
properties: [
{
name: 'accessToken',
value: '{fields.accessToken}',
name: 'consumerKey',
value: '{fields.consumerKey}',
},
{
name: 'consumerSecret',
value: '{fields.consumerSecret}',
},
],
},
@@ -85,6 +164,53 @@ export default {
{
step: 3,
type: 'mutation' as const,
name: 'createAuthData',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
],
},
{
step: 4,
type: 'openWithPopup' as const,
name: 'openAuthPopup',
arguments: [
{
name: 'url',
value: '{createAuthData.url}',
},
],
},
{
step: 5,
type: 'mutation' as const,
name: 'updateConnection',
arguments: [
{
name: 'id',
value: '{connection.id}',
},
{
name: 'formattedData',
value: null,
properties: [
{
name: 'code',
value: '{openAuthPopup.code}',
},
{
name: 'state',
value: '{openAuthPopup.state}',
},
],
},
],
},
{
step: 6,
type: 'mutation' as const,
name: 'verifyConnection',
arguments: [
{
@@ -95,6 +221,7 @@ export default {
},
],
createAuthData,
verifyCredentials,
isStillVerified,
};

View File

@@ -1,10 +1,10 @@
import { IGlobalVariable } from '@automatisch/types';
import verifyCredentials from './verify-credentials';
import getCurrentUser from '../common/get-current-user';
const isStillVerified = async ($: IGlobalVariable) => {
try {
await verifyCredentials($);
return true;
const user = await getCurrentUser($);
return !!user.id;
} catch (error) {
return false;
}

View File

@@ -1,34 +1,51 @@
import qs from 'qs';
import { IGlobalVariable } from '@automatisch/types';
import getCurrentUser from '../common/get-current-user';
const verifyCredentials = async ($: IGlobalVariable) => {
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
const oauthRedirectUrlField = $.app.auth.fields.find(
(field) => field.key == 'oAuthRedirectUrl'
);
const redirectUri = oauthRedirectUrlField.value as string;
const params = {
code: $.auth.data.code,
client_id: $.auth.data.consumerKey,
client_secret: $.auth.data.consumerSecret,
redirect_uri: redirectUri,
};
const stringifiedBody = qs.stringify({
token: $.auth.data.accessToken,
});
const response = await $.http.post('/auth.test', stringifiedBody, {
headers,
});
const response = await $.http.post('/oauth.v2.access', null, { params });
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)`
`Error occured while verifying credentials: ${response.data.error}. (More info: https://api.slack.com/methods/oauth.v2.access#errors)`
);
}
const { bot_id: botId, user: screenName } = response.data;
const {
bot_user_id: botId,
authed_user: {
id: userId,
access_token: userAccessToken,
},
access_token: botAccessToken,
team: {
name: teamName,
}
} = response.data;
$.auth.set({
await $.auth.set({
botId,
screenName,
userId,
userAccessToken,
botAccessToken,
screenName: teamName,
token: $.auth.data.accessToken,
});
return response.data;
const currentUser = await getCurrentUser($);
await $.auth.set({
screenName: `${currentUser.real_name} @ ${teamName}`
});
};
export default verifyCredentials;

View File

@@ -1,10 +1,21 @@
import { TBeforeRequest } from '@automatisch/types';
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
if (requestConfig.headers && $.auth.data?.accessToken) {
requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`;
const authData = $.auth.data;
if (
requestConfig.headers
&& authData?.userAccessToken
&& authData?.botAccessToken
) {
if (requestConfig.additionalProperties?.sendAsBot) {
requestConfig.headers.Authorization = `Bearer ${authData.botAccessToken}`;
} else {
requestConfig.headers.Authorization = `Bearer ${authData.userAccessToken}`;
}
}
requestConfig.headers['Content-Type'] = requestConfig.headers['Content-Type'] || 'application/json; charset=utf-8';
return requestConfig;
};

View File

@@ -0,0 +1,13 @@
import { IGlobalVariable, IJSONObject } from '@automatisch/types';
const getCurrentUser = async ($: IGlobalVariable): Promise<IJSONObject> => {
const params = {
user: $.auth.data.userId as string,
}
const response = await $.http.get('/users.info', { params });
const currentUser = response.data.user;
return currentUser;
};
export default getCurrentUser;

View File

@@ -38,6 +38,9 @@ export default function computeParameters(
};
}
return result;
return {
...result,
[key]: value,
};
}, {});
}

View File

@@ -6,18 +6,20 @@ This page explains the steps you need to follow to set up the Slack connection i
1. Go to the [link](https://api.slack.com/apps?new_app=1) to **create an app**
on Slack API.
2. Select **From scratch**.
3. Enter **App name**.
4. Pick the workspace you would like to use with the Slack connection.
5. Click on **Create App** button.
6. Click the **Permissions** card on the **Add features and functionality**
section.
7. Go to **User Token Scopes** and add the necessary scopes.
([more info](https://api.slack.com/scopes?filter=user))
8. Go to **OAuth Tokens for Your Workspace** and click **Install to Workspace**.
9. In **Where should Sample post?** section, select the channel you would like
to use with Automatisch, and click **Allow**.
10. Copy **User OAuth Token** and paste it to **Access Token** on the
Automatisch page.
11. Click **Submit** button on Automatisch.
12. Now, you can start using the Slack connection with Automatisch.
1. Select **From scratch**.
1. Enter **App name**.
1. Pick the workspace you would like to use with the Slack connection.
1. Click on **Create App** button.
1. Copy **Client ID** and **Client Secret** values and save them to use later.
1. Go to **OAuth & Permissions** page.
1. Copy **OAuth Redirect URL** from Automatisch and add it in Redirect URLs. Don't forget to save it after adding it by clicking **Save URLs** button!
1. Go to **Bot Token Scopes** and add `chat:write.customize` along with `chat:write` scope to enable the bot functionality.
:::warning HTTPS required!
Slack does **not** allow non-secure URLs in redirect URLs. Therefore, you will need to serve Automatisch via HTTPS protocol.
:::
10. Paste **Client ID** and **Client Secret** values you have saved earlier and paste them into Automatisch as **Consumer Key** and **Consumer Secret**, respectively.
1. Click **Submit** button on Automatisch.
1. Now, you can start using the Slack connection with Automatisch.

View File

@@ -301,4 +301,8 @@ declare module 'axios' {
interface AxiosResponse {
httpError?: IJSONObject;
}
interface AxiosRequestConfig {
additionalProperties?: Record<string, unknown>;
}
}