Merge pull request #673 from automatisch/issue-495
feat(slack): add bot capability in send message action
This commit is contained in:
@@ -32,13 +32,46 @@ export default defineAction({
|
|||||||
description: 'The content of your new message.',
|
description: 'The content of your new message.',
|
||||||
variables: true,
|
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($) {
|
async run($) {
|
||||||
const channelId = $.step.parameters.channel as string;
|
const message = await postMessage($);
|
||||||
const text = $.step.parameters.message as string;
|
|
||||||
|
|
||||||
const message = await postMessage($, channelId, text);
|
|
||||||
|
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
|
@@ -1,23 +1,54 @@
|
|||||||
import { IGlobalVariable } from '@automatisch/types';
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
import { URL } from 'url';
|
||||||
|
|
||||||
const postMessage = async (
|
type TData = {
|
||||||
$: IGlobalVariable,
|
channel: string;
|
||||||
channelId: string,
|
text: string;
|
||||||
text: string
|
username?: string;
|
||||||
) => {
|
icon_url?: string;
|
||||||
const params = {
|
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,
|
channel: channelId,
|
||||||
text,
|
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) {
|
if (response.data.ok === false) {
|
||||||
throw new Error(JSON.stringify(response.data));
|
throw new Error(JSON.stringify(response.data));
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = {
|
const message = {
|
||||||
raw: response?.data?.message,
|
raw: response?.data,
|
||||||
};
|
};
|
||||||
|
|
||||||
$.setActionItem(message);
|
$.setActionItem(message);
|
||||||
|
62
packages/backend/src/apps/slack/auth/create-auth-data.ts
Normal file
62
packages/backend/src/apps/slack/auth/create-auth-data.ts
Normal 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,
|
||||||
|
});
|
||||||
|
};
|
@@ -1,17 +1,41 @@
|
|||||||
|
import createAuthData from './create-auth-data';
|
||||||
import verifyCredentials from './verify-credentials';
|
import verifyCredentials from './verify-credentials';
|
||||||
import isStillVerified from './is-still-verified';
|
import isStillVerified from './is-still-verified';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
key: 'accessToken',
|
key: 'oAuthRedirectUrl',
|
||||||
label: 'Access Token',
|
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,
|
type: 'string' as const,
|
||||||
required: true,
|
required: true,
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
value: null,
|
value: null,
|
||||||
placeholder: 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,
|
clickToCopy: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -30,8 +54,12 @@ export default {
|
|||||||
value: null,
|
value: null,
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: 'accessToken',
|
name: 'consumerKey',
|
||||||
value: '{fields.accessToken}',
|
value: '{fields.consumerKey}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'consumerSecret',
|
||||||
|
value: '{fields.consumerSecret}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -40,6 +68,53 @@ export default {
|
|||||||
{
|
{
|
||||||
step: 2,
|
step: 2,
|
||||||
type: 'mutation' as const,
|
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',
|
name: 'verifyConnection',
|
||||||
arguments: [
|
arguments: [
|
||||||
{
|
{
|
||||||
@@ -75,8 +150,12 @@ export default {
|
|||||||
value: null,
|
value: null,
|
||||||
properties: [
|
properties: [
|
||||||
{
|
{
|
||||||
name: 'accessToken',
|
name: 'consumerKey',
|
||||||
value: '{fields.accessToken}',
|
value: '{fields.consumerKey}',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'consumerSecret',
|
||||||
|
value: '{fields.consumerSecret}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -85,6 +164,53 @@ export default {
|
|||||||
{
|
{
|
||||||
step: 3,
|
step: 3,
|
||||||
type: 'mutation' as const,
|
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',
|
name: 'verifyConnection',
|
||||||
arguments: [
|
arguments: [
|
||||||
{
|
{
|
||||||
@@ -95,6 +221,7 @@ export default {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
createAuthData,
|
||||||
verifyCredentials,
|
verifyCredentials,
|
||||||
isStillVerified,
|
isStillVerified,
|
||||||
};
|
};
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { IGlobalVariable } from '@automatisch/types';
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
import verifyCredentials from './verify-credentials';
|
import getCurrentUser from '../common/get-current-user';
|
||||||
|
|
||||||
const isStillVerified = async ($: IGlobalVariable) => {
|
const isStillVerified = async ($: IGlobalVariable) => {
|
||||||
try {
|
try {
|
||||||
await verifyCredentials($);
|
const user = await getCurrentUser($);
|
||||||
return true;
|
return !!user.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@@ -1,34 +1,51 @@
|
|||||||
import qs from 'qs';
|
|
||||||
import { IGlobalVariable } from '@automatisch/types';
|
import { IGlobalVariable } from '@automatisch/types';
|
||||||
|
import getCurrentUser from '../common/get-current-user';
|
||||||
|
|
||||||
const verifyCredentials = async ($: IGlobalVariable) => {
|
const verifyCredentials = async ($: IGlobalVariable) => {
|
||||||
const headers = {
|
const oauthRedirectUrlField = $.app.auth.fields.find(
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
(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 response = await $.http.post('/oauth.v2.access', null, { params });
|
||||||
const stringifiedBody = qs.stringify({
|
|
||||||
token: $.auth.data.accessToken,
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await $.http.post('/auth.test', stringifiedBody, {
|
|
||||||
headers,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.data.ok === false) {
|
if (response.data.ok === false) {
|
||||||
throw new Error(
|
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,
|
botId,
|
||||||
screenName,
|
userId,
|
||||||
|
userAccessToken,
|
||||||
|
botAccessToken,
|
||||||
|
screenName: teamName,
|
||||||
token: $.auth.data.accessToken,
|
token: $.auth.data.accessToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data;
|
const currentUser = await getCurrentUser($);
|
||||||
|
|
||||||
|
await $.auth.set({
|
||||||
|
screenName: `${currentUser.real_name} @ ${teamName}`
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export default verifyCredentials;
|
export default verifyCredentials;
|
||||||
|
@@ -1,10 +1,21 @@
|
|||||||
import { TBeforeRequest } from '@automatisch/types';
|
import { TBeforeRequest } from '@automatisch/types';
|
||||||
|
|
||||||
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
const addAuthHeader: TBeforeRequest = ($, requestConfig) => {
|
||||||
if (requestConfig.headers && $.auth.data?.accessToken) {
|
const authData = $.auth.data;
|
||||||
requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`;
|
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;
|
return requestConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
13
packages/backend/src/apps/slack/common/get-current-user.ts
Normal file
13
packages/backend/src/apps/slack/common/get-current-user.ts
Normal 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;
|
@@ -38,6 +38,9 @@ export default function computeParameters(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return {
|
||||||
|
...result,
|
||||||
|
[key]: value,
|
||||||
|
};
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
@@ -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**
|
1. Go to the [link](https://api.slack.com/apps?new_app=1) to **create an app**
|
||||||
on Slack API.
|
on Slack API.
|
||||||
2. Select **From scratch**.
|
1. Select **From scratch**.
|
||||||
3. Enter **App name**.
|
1. Enter **App name**.
|
||||||
4. Pick the workspace you would like to use with the Slack connection.
|
1. Pick the workspace you would like to use with the Slack connection.
|
||||||
5. Click on **Create App** button.
|
1. Click on **Create App** button.
|
||||||
6. Click the **Permissions** card on the **Add features and functionality**
|
1. Copy **Client ID** and **Client Secret** values and save them to use later.
|
||||||
section.
|
1. Go to **OAuth & Permissions** page.
|
||||||
7. Go to **User Token Scopes** and add the necessary scopes.
|
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!
|
||||||
([more info](https://api.slack.com/scopes?filter=user))
|
1. Go to **Bot Token Scopes** and add `chat:write.customize` along with `chat:write` scope to enable the bot functionality.
|
||||||
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
|
:::warning HTTPS required!
|
||||||
to use with Automatisch, and click **Allow**.
|
|
||||||
10. Copy **User OAuth Token** and paste it to **Access Token** on the
|
Slack does **not** allow non-secure URLs in redirect URLs. Therefore, you will need to serve Automatisch via HTTPS protocol.
|
||||||
Automatisch page.
|
:::
|
||||||
11. Click **Submit** button on Automatisch.
|
|
||||||
12. Now, you can start using the Slack connection with Automatisch.
|
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.
|
||||||
|
4
packages/types/index.d.ts
vendored
4
packages/types/index.d.ts
vendored
@@ -301,4 +301,8 @@ declare module 'axios' {
|
|||||||
interface AxiosResponse {
|
interface AxiosResponse {
|
||||||
httpError?: IJSONObject;
|
httpError?: IJSONObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AxiosRequestConfig {
|
||||||
|
additionalProperties?: Record<string, unknown>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user