Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,49 @@
import { slackAuth } from '../../';
import { createAction, Property } from '@activepieces/pieces-framework';
import { singleSelectChannelInfo, slackChannel } from '../common/props';
import { WebClient } from '@slack/web-api';
import { processMessageTimestamp } from '../common/utils';
export const addRectionToMessageAction = createAction({
auth: slackAuth,
name: 'slack-add-reaction-to-message',
displayName: 'Add Reaction to Message',
description: 'Add an emoji reaction to a message.',
props: {
info: singleSelectChannelInfo,
channel: slackChannel(true),
ts: Property.ShortText({
displayName: 'Message Timestamp',
description:
'Please provide the timestamp of the message you wish to react, such as `1710304378.475129`. Alternatively, you can easily obtain the message link by clicking on the three dots next to the message and selecting the `Copy link` option.',
required: true,
}),
reaction: Property.ShortText({
displayName: 'Reaction (emoji) name',
required: true,
description: 'e.g.`thumbsup`',
}),
},
async run(context) {
const { channel, ts, reaction } = context.propsValue;
const slack = new WebClient(context.auth.access_token);
const messageTimestamp = processMessageTimestamp(ts);
if (messageTimestamp) {
const response = await slack.reactions.add({
channel,
timestamp: messageTimestamp,
name: reaction,
});
return response;
} else {
throw new Error('Invalid Timestamp Value.');
}
},
});

View File

@@ -0,0 +1,28 @@
import { slackAuth } from '../../';
import { createAction, Property } from '@activepieces/pieces-framework';
import { WebClient } from '@slack/web-api';
export const createChannelAction = createAction({
auth: slackAuth,
name: 'slack-create-channel',
displayName: 'Create Channel',
description: 'Creates a new channel.',
props: {
channelName: Property.ShortText({
displayName: 'Channel Name',
required: true,
}),
isPrivate: Property.Checkbox({
displayName: 'Is Private?',
required: false,
defaultValue: false,
}),
},
async run({ auth, propsValue }) {
const client = new WebClient(auth.access_token);
return await client.conversations.create({
name: propsValue.channelName,
is_private: propsValue.isPrivate,
});
},
});

View File

@@ -0,0 +1,23 @@
import { slackAuth } from '../../';
import { createAction, Property } from '@activepieces/pieces-framework';
import { WebClient } from '@slack/web-api';
export const findUserByEmailAction = createAction({
auth: slackAuth,
name: 'slack-find-user-by-email',
displayName: 'Find User by Email',
description: 'Finds a user by matching against their email address.',
props: {
email: Property.ShortText({
displayName: 'Email',
required: true,
}),
},
async run({ auth, propsValue }) {
const email = propsValue.email;
const client = new WebClient(auth.access_token);
return await client.users.lookupByEmail({
email,
});
},
});

View File

@@ -0,0 +1,35 @@
import { slackAuth } from '../../';
import { createAction, Property } from '@activepieces/pieces-framework';
import { UsersListResponse, WebClient } from '@slack/web-api';
export const findUserByHandleAction = createAction({
auth: slackAuth,
name: 'slack-find-user-by-handle',
displayName: 'Find User by Handle',
description: 'Finds a user by matching against their Slack handle.',
props: {
handle: Property.ShortText({
displayName: 'Handle',
description: 'User handle (display name), without the leading @',
required: true,
}),
},
async run({ auth, propsValue }) {
const handle = propsValue.handle.replace('@', '');
const client = new WebClient(auth.access_token);
for await (const page of client.paginate('users.list', {
limit: 1000, // Only limits page size, not total number of results
})) {
const response = page as UsersListResponse;
if (response.members) {
const matchedMember = response.members.find(
(member) => member.profile?.display_name === handle
);
if (matchedMember) {
return matchedMember;
}
}
}
throw new Error(`Could not find user with handle @${handle}`);
},
});

View File

@@ -0,0 +1,22 @@
import { slackAuth } from '../../';
import { createAction, Property } from '@activepieces/pieces-framework';
import { WebClient } from '@slack/web-api';
export const findUserByIdAction = createAction({
auth: slackAuth,
name: 'find-user-by-id',
displayName: 'Find User by ID',
description: 'Finds a user by their ID.',
props: {
id: Property.ShortText({
displayName: 'ID',
required: true,
}),
},
async run({ auth, propsValue }) {
const client = new WebClient(auth.access_token);
return await client.users.profile.get({
user: propsValue.id,
});
},
});

View File

@@ -0,0 +1,61 @@
import { ConversationsHistoryResponse, WebClient } from '@slack/web-api';
import { createAction, Property } from '@activepieces/pieces-framework';
import { slackAuth } from '../..';
import { singleSelectChannelInfo, slackChannel } from '../common/props';
export const getChannelHistory = createAction({
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
name: 'getChannelHistory',
auth: slackAuth,
displayName: 'Get channel history',
description:
'Retrieve all messages from a specific channel ("conversation") between specified timestamps',
props: {
info: singleSelectChannelInfo,
channel: slackChannel(true),
oldest: Property.Number({
displayName: 'Oldest',
description:
'Only messages after this timestamp will be included in results',
required: false,
}),
latest: Property.Number({
displayName: 'Latest',
description:
'Only messages before this timestamp will be included in results. Default is the current time',
required: false,
}),
inclusive: Property.Checkbox({
displayName: 'Inclusive',
description:
'Include messages with oldest or latest timestamps in results. Ignored unless either timestamp is specified',
defaultValue: false,
required: true,
}),
includeAllMetadata: Property.Checkbox({
displayName: 'Include all metadata',
description: 'Return all metadata associated with each message',
defaultValue: false,
required: true,
}),
},
async run({ auth, propsValue }) {
const client = new WebClient(auth.access_token);
const messages = [];
await client.conversations.history({ channel: propsValue.channel });
for await (const page of client.paginate('conversations.history', {
channel: propsValue.channel,
oldest: propsValue.oldest,
latest: propsValue.latest,
limit: 200, // page size, does not limit the total number of results
include_all_metadata: propsValue.includeAllMetadata,
inclusive: propsValue.inclusive,
})) {
const response = page as ConversationsHistoryResponse;
if (response.messages) {
messages.push(...response.messages);
}
}
return messages;
},
});

View File

@@ -0,0 +1,47 @@
import { slackAuth } from '../../index';
import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { WebClient } from '@slack/web-api';
export const getFileAction = createAction({
auth: slackAuth,
name: 'get-file',
displayName: 'Get File',
description: 'Return information about a given file ID.',
props: {
fileId: Property.ShortText({
displayName: 'File ID',
required: true,
description: 'You can pass the file ID from the New Message Trigger payload.',
}),
},
async run(context) {
const client = new WebClient(context.auth.access_token);
const fileData = await client.files.info({ file: context.propsValue.fileId });
const fileDownloadUrl = fileData.file?.url_private_download;
if (!fileDownloadUrl) {
throw new Error('Unable to find the download URL.');
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: fileDownloadUrl,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: context.auth.access_token,
},
responseType: 'arraybuffer',
});
return {
...fileData.file,
data: await context.files.write({
fileName: fileData.file?.name || `file`,
data: Buffer.from(response.body),
}),
};
},
});

View File

@@ -0,0 +1,36 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { slackAuth } from '../..';
import { singleSelectChannelInfo, slackChannel } from '../common/props';
import { processMessageTimestamp } from '../common/utils';
import { WebClient } from '@slack/web-api';
export const getMessageAction = createAction({
name: 'get-message',
displayName: 'Get Message by Timestamp',
description: `Retrieves a specific message from a channel history using the message's timestamp.`,
auth: slackAuth,
props: {
info: singleSelectChannelInfo,
channel: slackChannel(true),
ts: Property.ShortText({
displayName: 'Message Timestamp',
description:
'Please provide the timestamp of the message you wish to retrieve, such as `1710304378.475129`. Alternatively, you can easily obtain the message link by clicking on the three dots next to the message and selecting the `Copy link` option.',
required: true,
}),
},
async run({ auth, propsValue }) {
const messageTimestamp = processMessageTimestamp(propsValue.ts);
if (!messageTimestamp) {
throw new Error('Invalid Timestamp Value.');
}
const client = new WebClient(auth.access_token);
return await client.conversations.history({
channel: propsValue.channel,
latest: messageTimestamp,
limit: 1,
inclusive: true,
});
},
});

View File

@@ -0,0 +1,24 @@
import { slackAuth } from '../../index';
import { createAction } from '@activepieces/pieces-framework';
import { singleSelectChannelInfo, slackChannel, userId } from '../common/props';
import { WebClient } from '@slack/web-api';
export const inviteUserToChannelAction = createAction({
auth: slackAuth,
name: 'invite-user-to-channel',
displayName: 'Invite User to Channel',
description: 'Invites an existing User to an existing channel.',
props: {
info: singleSelectChannelInfo,
channel: slackChannel(true),
userId,
},
async run(context) {
const client = new WebClient(context.auth.access_token);
return await client.conversations.invite({
channel: context.propsValue.channel,
users: `${context.propsValue.userId}`,
});
},
});

View File

@@ -0,0 +1,43 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { UsersListResponse, WebClient } from '@slack/web-api';
import { slackAuth } from '../..';
import { Member } from '@slack/web-api/dist/types/response/UsersListResponse';
export const listUsers = createAction({
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
name: 'listUsers',
displayName: 'List users',
description: 'List all users of the workspace',
props: {
includeBots: Property.Checkbox({
displayName: 'Include bots?',
required: true,
defaultValue: false,
}),
includeDisabled: Property.Checkbox({
displayName: 'Include disabled users?',
required: true,
defaultValue: false,
}),
},
auth: slackAuth,
async run({ auth, propsValue }) {
const client = new WebClient(auth.access_token);
const results: Member[] = [];
for await (const page of client.paginate('users.list', {
limit: 1000, // Only limits page size, not total number of results
})) {
const response = page as UsersListResponse;
if (response.members) {
results.push(
...response.members.filter(
(member) =>
(propsValue.includeDisabled || !member.deleted) &&
(propsValue.includeBots || !member.is_bot)
)
);
}
}
return results;
},
});

View File

@@ -0,0 +1,23 @@
import {
createAction,
Property,
} from '@activepieces/pieces-framework';
import slackifyMarkdown from 'slackify-markdown';
export const markdownToSlackFormat = createAction({
name: 'markdownToSlackFormat',
displayName: 'Markdown to Slack format',
description:
"Convert Markdown-formatted text to Slack's pseudo - markdown syntax",
requireAuth: false,
props: {
markdown: Property.LongText({
displayName: 'Markdown text',
required: true,
}),
},
async run({ propsValue }) {
return slackifyMarkdown(propsValue.markdown);
},
});

View File

@@ -0,0 +1,32 @@
import { createAction } from '@activepieces/pieces-framework';
import { slackAuth } from '../..';
import { assertNotNullOrUndefined } from '@activepieces/shared';
import {
profilePicture,
text,
userId,
username,
actions,
} from '../common/props';
import { requestAction } from '../common/request-action';
export const requestActionDirectMessageAction = createAction({
auth: slackAuth,
name: 'request_action_direct_message',
displayName: 'Request Action from A User',
description:
'Send a message to a user and wait until the user selects an action',
props: {
userId,
text,
actions,
username,
profilePicture,
},
async run(context) {
const { userId } = context.propsValue;
assertNotNullOrUndefined(userId, 'userId');
return await requestAction(userId, context);
},
});

View File

@@ -0,0 +1,36 @@
import { createAction } from '@activepieces/pieces-framework';
import { slackAuth } from '../..';
import { assertNotNullOrUndefined } from '@activepieces/shared';
import {
profilePicture,
text,
slackChannel,
username,
actions,
singleSelectChannelInfo,
threadTs,
} from '../common/props';
import { requestAction } from '../common/request-action';
export const requestActionMessageAction = createAction({
auth: slackAuth,
name: 'request_action_message',
displayName: 'Request Action in A Channel',
description:
'Send a message in a channel and wait until an action is selected',
props: {
info: singleSelectChannelInfo,
channel: slackChannel(true),
text,
actions,
threadTs,
username,
profilePicture,
},
async run(context) {
const { channel } = context.propsValue;
assertNotNullOrUndefined(channel, 'channel');
return await requestAction(channel, context);
},
});

View File

@@ -0,0 +1,110 @@
import { createAction } from '@activepieces/pieces-framework';
import { slackSendMessage } from '../common/utils';
import { slackAuth } from '../..';
import {
assertNotNullOrUndefined,
ExecutionType,
PauseType,
} from '@activepieces/shared';
import { profilePicture, text, userId, username } from '../common/props';
import { ChatPostMessageResponse, WebClient } from '@slack/web-api';
export const requestApprovalDirectMessageAction = createAction({
auth: slackAuth,
name: 'request_approval_direct_message',
displayName: 'Request Approval from A User',
description:
'Send approval message to a user and then wait until the message is approved or disapproved',
props: {
userId,
text,
username,
profilePicture,
},
async run(context) {
if (context.executionType === ExecutionType.BEGIN) {
const token = context.auth.access_token;
const { userId, username, profilePicture } = context.propsValue;
assertNotNullOrUndefined(token, 'token');
assertNotNullOrUndefined(text, 'text');
assertNotNullOrUndefined(userId, 'userId');
const postMessage = await slackSendMessage({
token,
text: `${context.propsValue.text}`,
username,
profilePicture,
conversationId: userId,
});
const dmId = (postMessage as ChatPostMessageResponse).channel as string;
const messageTs = (postMessage as ChatPostMessageResponse).ts as string
const approvalLink = context.generateResumeUrl({
queryParams: { action: 'approve',messageTs },
});
const disapprovalLink = context.generateResumeUrl({
queryParams: { action: 'disapprove',messageTs },
});
const client = new WebClient(token);
await client.chat.update({
ts:messageTs,
channel:dmId,
text: context.propsValue.text,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${context.propsValue.text}`,
},
},
{
type: 'actions',
block_id: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: 'Approve',
},
style: 'primary',
url: approvalLink,
},
{
type: 'button',
text: {
type: 'plain_text',
text: 'Disapprove',
},
style: 'danger',
url: disapprovalLink,
},
],
},
],
});
context.run.pause({
pauseMetadata: {
type: PauseType.WEBHOOK,
response: {},
},
});
return {
approved: false, // default approval is false
messageTs
};
} else {
return {
approved: context.resumePayload.queryParams['action'] === 'approve',
messageTs: context.resumePayload.queryParams['messageTs']
};
}
},
});

View File

@@ -0,0 +1,114 @@
import { createAction } from '@activepieces/pieces-framework';
import { slackSendMessage } from '../common/utils';
import { slackAuth } from '../..';
import {
assertNotNullOrUndefined,
ExecutionType,
PauseType,
} from '@activepieces/shared';
import {
profilePicture,
singleSelectChannelInfo,
slackChannel,
text,
username,
} from '../common/props';
import { ChatPostMessageResponse, WebClient } from '@slack/web-api';
export const requestSendApprovalMessageAction = createAction({
auth: slackAuth,
name: 'request_approval_message',
displayName: 'Request Approval in a Channel',
description:
'Send approval message to a channel and then wait until the message is approved or disapproved',
props: {
info: singleSelectChannelInfo,
channel: slackChannel(true),
text,
username,
profilePicture,
},
async run(context) {
if (context.executionType === ExecutionType.BEGIN) {
const token = context.auth.access_token;
const { channel, username, profilePicture } = context.propsValue;
assertNotNullOrUndefined(token, 'token');
assertNotNullOrUndefined(text, 'text');
assertNotNullOrUndefined(channel, 'channel');
const postMessage = await slackSendMessage({
token,
text: `${context.propsValue.text}`,
username,
profilePicture,
conversationId: channel,
});
const messageTs = (postMessage as ChatPostMessageResponse).ts as string
const approvalLink = context.generateResumeUrl({
queryParams: { action: 'approve', channel, messageTs },
});
const disapprovalLink = context.generateResumeUrl({
queryParams: { action: 'disapprove', channel, messageTs },
});
const client = new WebClient(token);
await client.chat.update({
channel: channel,
ts: messageTs,
text: context.propsValue.text,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${context.propsValue.text}`,
},
},
{
type: 'actions',
block_id: 'actions',
elements: [
{
type: 'button',
text: {
type: 'plain_text',
text: 'Approve',
},
style: 'primary',
url: approvalLink,
},
{
type: 'button',
text: {
type: 'plain_text',
text: 'Disapprove',
},
style: 'danger',
url: disapprovalLink,
},
],
},
],
});
context.run.pause({
pauseMetadata: {
type: PauseType.WEBHOOK,
response: {},
},
});
return {
approved: false, // default approval is false
messageTs
};
} else {
return {
approved: context.resumePayload.queryParams['action'] === 'approve',
messageTs: context.resumePayload.queryParams['messageTs']
};
}
},
});

View File

@@ -0,0 +1,32 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { slackAuth } from '../..';
import { WebClient } from '@slack/web-api';
import { slackChannel } from '../common/props';
import { processMessageTimestamp } from '../common/utils';
export const retrieveThreadMessages = createAction({
name: 'retrieveThreadMessages',
displayName: 'Retrieve Thread Messages',
description: 'Retrieves thread messages by channel and thread timestamp.',
auth: slackAuth,
props: {
channel: slackChannel(true),
threadTs: Property.ShortText({
displayName: 'Thread ts',
description:
'Provide the ts (timestamp) value of the **parent** message to retrieve replies of this message. Do not use the ts value of the reply itself; use its parent instead. For example `1710304378.475129`.Alternatively, you can easily obtain the message link by clicking on the three dots next to the parent message and selecting the `Copy link` option.',
required: true,
}),
},
async run({ auth, propsValue }) {
const client = new WebClient(auth.access_token);
const messageTimestamp = processMessageTimestamp(propsValue.threadTs);
if (!messageTimestamp) {
throw new Error('Invalid Timestamp Value.');
}
return await client.conversations.replies({
channel: propsValue.channel,
ts: messageTimestamp,
});
},
});

View File

@@ -0,0 +1,49 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { slackAuth } from '../..';
import { WebClient } from '@slack/web-api';
export const searchMessages = createAction({
name: 'searchMessages',
displayName: 'Search messages',
description: 'Searches for messages matching a query',
auth: slackAuth,
props: {
query: Property.ShortText({
displayName: 'Search query',
required: true,
}),
},
async run({ auth, propsValue }) {
const userToken = auth.data['authed_user']?.access_token;
if (userToken === undefined) {
throw new Error(JSON.stringify(
{
message: 'Missing user token, please re-authenticate'
}
));
}
const client = new WebClient(userToken);
const matches = [];
// We can't use the usual "for await ... of" syntax with client.paginate
// Because search.messages uses a bastardized version of cursor-based pagination
// Where you need to pass * as first cursor
// https://api.slack.com/methods/search.messages#arg_cursor
let cursor = '*';
do {
const page = await client.search.messages({
query: propsValue.query,
count: 100,
// @ts-expect-error TS2353 - SDK is not aware cursor is actually supported
cursor,
});
if (page.messages?.matches) {
matches.push(...page.messages.matches);
}
// @ts-expect-error TS2353 - SDK is not aware next_cursor is actually returned
cursor = page.messages?.pagination?.next_cursor;
} while (cursor);
return matches;
},
});

View File

@@ -0,0 +1,69 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { slackSendMessage } from '../common/utils';
import { slackAuth } from '../../';
import { assertNotNullOrUndefined } from '@activepieces/shared';
import {
profilePicture,
text,
userId,
username,
blocks,
mentionOriginFlow,
} from '../common/props';
import { Block,KnownBlock } from '@slack/web-api';
export const slackSendDirectMessageAction = createAction({
auth: slackAuth,
name: 'send_direct_message',
displayName: 'Send Message To A User',
description: 'Send message to a user',
props: {
userId,
text,
username,
profilePicture,
mentionOriginFlow,
blocks,
unfurlLinks: Property.Checkbox({
displayName: 'Unfurl Links',
description: 'Enable link unfurling for this message',
required: false,
defaultValue: true,
}),
},
async run(context) {
const token = context.auth.access_token;
const { text, userId, blocks, unfurlLinks, mentionOriginFlow } = context.propsValue;
assertNotNullOrUndefined(token, 'token');
assertNotNullOrUndefined(text, 'text');
assertNotNullOrUndefined(userId, 'userId');
const blockList: (KnownBlock | Block)[] = [{ type: 'section', text: { type: 'mrkdwn', text } }]
if(blocks && Array.isArray(blocks)) {
blockList.push(...(blocks as unknown as (KnownBlock | Block)[]))
}
if(mentionOriginFlow) {
(blockList as KnownBlock[])?.push({ type: 'context', elements: [
{
"type": "mrkdwn",
"text": `Message sent by <${new URL(context.server.publicUrl).origin}/projects/${context.project.id}/flows/${context.flows.current.id}|this flow>.`
}
] })
}
return slackSendMessage({
token,
text,
username: context.propsValue.username,
profilePicture: context.propsValue.profilePicture,
conversationId: userId,
blocks:blockList,
unfurlLinks,
});
},
});

View File

@@ -0,0 +1,93 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
profilePicture,
slackChannel,
username,
blocks,
threadTs,
singleSelectChannelInfo,
mentionOriginFlow,
} from '../common/props';
import { processMessageTimestamp, slackSendMessage } from '../common/utils';
import { slackAuth } from '../../';
import { Block,KnownBlock } from '@slack/web-api';
export const slackSendMessageAction = createAction({
auth: slackAuth,
name: 'send_channel_message',
displayName: 'Send Message To A Channel',
description: 'Send message to a channel',
props: {
info: singleSelectChannelInfo,
channel: slackChannel(true),
text: Property.LongText({
displayName: 'Message',
description: 'The text of your message. When using Block Kit blocks, this is used as a fallback for notifications.',
required: false,
}),
threadTs,
username,
profilePicture,
file: Property.File({
displayName: 'Attachment',
required: false,
}),
replyBroadcast: Property.Checkbox({
displayName: 'Broadcast reply to channel',
description: 'When replying to a thread, also make the message visible to everyone in the channel (only applicable when Thread Timestamp is provided)',
required: false,
defaultValue: false,
}),
mentionOriginFlow,
unfurlLinks: Property.Checkbox({
displayName: 'Unfurl Links',
description: 'Enable link unfurling for this message',
required: false,
defaultValue: true,
}),
blocks,
},
async run(context) {
const token = context.auth.access_token;
const { text, channel, username, profilePicture, threadTs, file, mentionOriginFlow, blocks, replyBroadcast, unfurlLinks } =
context.propsValue;
if (!text && (!blocks || !Array.isArray(blocks) || blocks.length === 0)) {
throw new Error('Either Message or Block Kit blocks must be provided');
}
const blockList: (KnownBlock | Block)[] = [];
if (text && (!blocks || !Array.isArray(blocks) || blocks.length === 0)) {
blockList.push({ type: 'section', text: { type: 'mrkdwn', text } });
}
if(blocks && Array.isArray(blocks) && blocks.length > 0) {
blockList.push(...(blocks as unknown as (KnownBlock | Block)[]))
}
if(mentionOriginFlow) {
(blockList as KnownBlock[])?.push({ type: 'context', elements: [
{
"type": "mrkdwn",
"text": `Message sent by <${new URL(context.server.publicUrl).origin}/projects/${context.project.id}/flows/${context.flows.current.id}|this flow>.`
}
] })
}
return slackSendMessage({
token,
text: text || undefined,
username,
profilePicture,
conversationId: channel,
threadTs: threadTs ? processMessageTimestamp(threadTs) : undefined,
file,
blocks: blockList.length > 0 ? blockList : undefined,
replyBroadcast,
unfurlLinks,
});
},
});

View File

@@ -0,0 +1,28 @@
import { slackAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { singleSelectChannelInfo, slackChannel } from '../common/props';
import { WebClient } from '@slack/web-api';
export const setChannelTopicAction = createAction({
auth: slackAuth,
name: 'set-channel-topic',
displayName: 'Set Channel Topic',
description: 'Sets the topic on a selected channel.',
props: {
info: singleSelectChannelInfo,
channel: slackChannel(true),
topic: Property.LongText({
displayName: 'Topic',
required: true,
}),
},
async run(context) {
const { channel, topic } = context.propsValue;
const client = new WebClient(context.auth.access_token);
return await client.conversations.setTopic({
channel,
topic,
});
},
});

View File

@@ -0,0 +1,43 @@
import { slackAuth } from '../../';
import { createAction, Property } from '@activepieces/pieces-framework';
import { WebClient } from '@slack/web-api';
import { z } from 'zod';
import { propsValidation } from '@activepieces/pieces-common';
export const setUserStatusAction = createAction({
auth: slackAuth,
name: 'slack-set-user-status',
displayName: 'Set User Status',
description: "Sets a user's custom status",
props: {
text: Property.ShortText({
displayName: 'Text',
required: true,
}),
emoji: Property.ShortText({
displayName: 'Emoji',
required: false,
description:
'Emoji shortname (standard or custom), e.g. :tada: or :train:',
}),
expiration: Property.Number({
displayName: 'Expires at',
description: 'Unix timestamp - if not set, the status will not expire',
required: false,
}),
},
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, {
text: z.string().max(100),
});
const client = new WebClient(auth.data['authed_user']?.access_token);
return await client.users.profile.set({
profile: {
status_text: propsValue.text,
status_emoji: propsValue.emoji,
status_expiration: propsValue.expiration,
},
});
},
});

View File

@@ -0,0 +1,46 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { slackAuth } from '../..';
import { blocks, singleSelectChannelInfo, slackChannel } from '../common/props';
import { processMessageTimestamp } from '../common/utils';
import { Block,KnownBlock, WebClient } from '@slack/web-api';
export const updateMessage = createAction({
// auth: check https://www.activepieces.com/docs/developers/piece-reference/authentication,
name: 'updateMessage',
displayName: 'Update message',
description: 'Update an existing message',
auth: slackAuth,
props: {
info: singleSelectChannelInfo,
channel: slackChannel(true),
ts: Property.ShortText({
displayName: 'Message Timestamp',
description:
'Please provide the timestamp of the message you wish to update, such as `1710304378.475129`. Alternatively, you can easily obtain the message link by clicking on the three dots next to the message and selecting the `Copy link` option.',
required: true,
}),
text: Property.LongText({
displayName: 'Message',
description: 'The updated text of your message',
required: true,
}),
blocks,
},
async run({ auth, propsValue }) {
const messageTimestamp = processMessageTimestamp(propsValue.ts);
if (!messageTimestamp) {
throw new Error('Invalid Timestamp Value.');
}
const client = new WebClient(auth.access_token);
const blockList = propsValue.blocks ?[{ type: 'section', text: { type: 'mrkdwn', text:propsValue.text } }, ...(propsValue.blocks as unknown as (KnownBlock | Block)[])] :undefined
return await client.chat.update({
channel: propsValue.channel,
ts: messageTimestamp,
text: propsValue.text,
blocks: blockList,
});
},
});

View File

@@ -0,0 +1,42 @@
import { slackAuth } from '../../';
import { createAction, Property } from '@activepieces/pieces-framework';
import { WebClient } from '@slack/web-api';
export const updateProfileAction = createAction({
auth: slackAuth,
name: 'slack-update-profile',
displayName: 'Update Profile',
description: 'Update basic profile field such as name or title.',
props: {
firstName: Property.ShortText({
displayName: 'First Name',
required: false,
}),
lastName: Property.ShortText({
displayName: 'Last Name',
required: false,
}),
email: Property.ShortText({
displayName: 'Email',
description: `Changing a user's email address will send an email to both the old and new addresses, and also post a slackbot message to the user informing them of the change.`,
required: false,
}),
userId: Property.ShortText({
displayName: 'User',
description:
'ID of user to change. This argument may only be specified by admins on paid teams.You can use **Find User by Email** action to retrieve ID.',
required: false,
}),
},
async run({ auth, propsValue }) {
const client = new WebClient(auth.data['authed_user']?.access_token);
return client.users.profile.set({
profile: {
first_name: propsValue.firstName,
last_name: propsValue.lastName,
email: propsValue.email,
},
user: propsValue.userId,
});
},
});

View File

@@ -0,0 +1,38 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { slackAuth } from '../../index';
import { WebClient } from '@slack/web-api';
import {
slackChannel,
} from '../common/props';
export const uploadFile = createAction({
auth: slackAuth,
name: 'uploadFile',
displayName: 'Upload file',
description: 'Upload file without sharing it to a channel or user',
props: {
file: Property.File({
displayName: 'Attachment',
required: true,
}),
title: Property.ShortText({
displayName: 'Title',
required: false,
}),
filename: Property.ShortText({
displayName: 'Filename',
required: false,
}),
channel: slackChannel(false),
},
async run(context) {
const token = context.auth.access_token;
const { file, title, filename, channel } = context.propsValue;
const client = new WebClient(token);
return await client.files.uploadV2({
file_uploads: [{ file: file.data, filename: filename || file.filename }],
title: title,
channel_id: channel,
});
},
});

View File

@@ -0,0 +1,181 @@
import { OAuth2PropertyValue, Property } from '@activepieces/pieces-framework';
import { UsersListResponse, WebClient } from '@slack/web-api';
import {slackAuth} from '../../index';
const slackChannelBotInstruction = `
Please make sure add the bot to the channel by following these steps:
1. Type /invite in the channel's chat.
2. Click on Add apps to this channel.
3. Search for and add the bot.
`;
export const multiSelectChannelInfo = Property.MarkDown({
value:
slackChannelBotInstruction +
`\n**Note**: If you can't find the channel in the dropdown list (which fetches up to 2000 channels), please click on the **(F)** and type the channel ID directly in an array like this: \`{\`{ ['your_channel_id_1', 'your_channel_id_2', ...] \`}\`}`,
});
export const singleSelectChannelInfo = Property.MarkDown({
value:
slackChannelBotInstruction +
`\n**Note**: If you can't find the channel in the dropdown list (which fetches up to 2000 channels), please click on the **(F)** and type the channel ID directly.
`,
});
export const slackChannel = <R extends boolean>(required: R) =>
Property.Dropdown<string, R,typeof slackAuth>({
auth: slackAuth,
displayName: 'Channel',
description:
"You can get the Channel ID by right-clicking on the channel and selecting 'View Channel Details.'",
required,
refreshers: [],
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'connect slack account',
options: [],
};
}
const authentication = auth as OAuth2PropertyValue;
const accessToken = authentication['access_token'];
const channels = await getChannels(accessToken);
return {
disabled: false,
placeholder: 'Select channel',
options: channels,
};
},
});
export const username = Property.ShortText({
displayName: 'Username',
description: 'The username of the bot',
required: false,
});
export const profilePicture = Property.ShortText({
displayName: 'Profile Picture',
description: 'The profile picture of the bot',
required: false,
});
export const threadTs = Property.ShortText({
displayName: 'Reply to Thread (Thread Message Link/Timestamp)',
description:
'Provide the ts (timestamp) or link value of the **parent** message to make this message a reply. Do not use the ts value of the reply itself; use its parent instead. For example `1710304378.475129`.Alternatively, you can easily obtain the message link by clicking on the three dots next to the parent message and selecting the `Copy link` option.',
required: false,
});
export const mentionOriginFlow = Property.Checkbox({
displayName: 'Mention flow of origin?',
description:
'If checked, adds a mention at the end of the Slack message to indicate which flow sent the notification, with a link to said flow.',
required: false,
defaultValue: false,
});
export const blocks = Property.Json({
displayName: 'Block Kit blocks',
description: 'See https://api.slack.com/block-kit for specs',
required: false,
defaultValue: []
});
export const userId = Property.Dropdown<string,true,typeof slackAuth>({
auth: slackAuth,
displayName: 'User',
required: true,
refreshers: [],
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'connect slack account',
options: [],
};
}
const accessToken = (auth as OAuth2PropertyValue).access_token;
const client = new WebClient(accessToken);
const users: { label: string; value: string }[] = [];
for await (const page of client.paginate('users.list', {
limit: 1000, // Only limits page size, not total number of results
})) {
const response = page as UsersListResponse;
if (response.members) {
users.push(
...response.members
.filter((member) => !member.deleted)
.map((member) => {
return { label: member.name || '', value: member.id || '' };
})
);
}
}
return {
disabled: false,
placeholder: 'Select User',
options: users,
};
},
});
export const text = Property.LongText({
displayName: 'Message',
required: true,
});
export const actions = Property.Array({
displayName: 'Action Buttons',
required: true,
properties: {
label: Property.ShortText({
displayName: 'Label',
required: true,
}),
style: Property.StaticDropdown({
displayName: 'Style',
required: false,
defaultValue: null,
options: {
options: [
{ label: 'Default', value: null },
{ label: 'Primary', value: 'primary' },
{ label: 'Danger', value: 'danger' },
],
},
}),
},
});
export async function getChannels(accessToken: string) {
const client = new WebClient(accessToken);
const channels: { label: string; value: string }[] = [];
const CHANNELS_LIMIT = 2000;
let cursor;
do {
const response = await client.conversations.list({
types: 'public_channel,private_channel',
exclude_archived: true,
limit: 1000,
cursor,
});
if (response.channels) {
channels.push(
...response.channels.map((channel) => {
return { label: channel.name || '', value: channel.id || '' };
})
);
}
cursor = response.response_metadata?.next_cursor;
} while (cursor && channels.length < CHANNELS_LIMIT);
return channels;
}

View File

@@ -0,0 +1,140 @@
import { processMessageTimestamp, slackSendMessage } from './utils';
import {
assertNotNullOrUndefined,
ExecutionType,
PauseType,
} from '@activepieces/shared';
import { ChatPostMessageResponse } from '@slack/web-api';
export const requestAction = async (conversationId: string, context: any) => {
const { actions } = context.propsValue;
assertNotNullOrUndefined(actions, 'actions');
if (!actions.length) {
throw new Error(`Must have at least one button action`);
}
const actionTextToIds = actions.map(
({ label, style }: { label: string; style: string }) => {
if (!label) {
throw new Error(`Button text for the action cannot be empty`);
}
return {
label,
style,
actionId: encodeURI(label as string),
};
}
);
if (context.executionType === ExecutionType.BEGIN) {
context.run.pause({
pauseMetadata: {
type: PauseType.WEBHOOK,
actions: actionTextToIds.map(
(action: { actionId: string }) => action.actionId
),
},
});
const token = context.auth.access_token;
const { text, username, profilePicture } = context.propsValue;
assertNotNullOrUndefined(token, 'token');
assertNotNullOrUndefined(text, 'text');
const actionElements = actionTextToIds.map(
(action: { label: string; style: string; actionId: string }) => {
const actionLink = context.generateResumeUrl({
queryParams: { action: action.actionId },
});
return {
type: 'button',
text: {
type: 'plain_text',
text: action.label,
},
...(action.style && {style: action.style}),
value: actionLink,
action_id: action.actionId,
};
}
);
const messageResponse: ChatPostMessageResponse = await slackSendMessage({
token,
text: `${context.propsValue.text}`,
username,
profilePicture,
threadTs: context.propsValue.threadTs
? processMessageTimestamp(context.propsValue.threadTs)
: undefined,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${context.propsValue.text}`,
},
},
{
type: 'actions',
block_id: 'actions',
elements: actionElements,
},
],
conversationId: conversationId,
});
return {
action: actionTextToIds.at(0) || 'N/A',
payload: {
type: 'action_blocks',
user: {
id: messageResponse.message?.user,
username: 'user.name',
name: 'john.smith',
team_id: messageResponse.message?.team,
},
container: {
type: 'message',
message_ts: messageResponse.ts,
channel_id: messageResponse.channel,
is_ephemeral: false,
},
trigger_id: 'trigger_id',
team: {
id: messageResponse.message?.team,
domain: 'team_name',
},
channel: {
id: messageResponse.channel,
name: '#channel',
},
message: messageResponse.message,
state: {},
actions: [
{
action_id: 'action_id',
block_id: 'actions',
value: 'resume_url',
style: 'primary',
type: 'button',
action_ts: 'action_ts',
},
],
},
};
} else {
const payloadQueryParams = context.resumePayload.queryParams as {
action: string;
};
return {
action: decodeURI(payloadQueryParams.action),
payload: context.resumePayload.body,
};
}
};

View File

@@ -0,0 +1,130 @@
import { ApFile } from '@activepieces/pieces-framework';
import { Block, WebClient } from '@slack/web-api';
export const slackSendMessage = async ({
text,
conversationId,
username,
profilePicture,
blocks,
threadTs,
token,
file,
replyBroadcast,
unfurlLinks,
}: SlackSendMessageParams) => {
const client = new WebClient(token);
if (file) {
return await client.files.uploadV2({
channel_id: conversationId,
initial_comment: text,
thread_ts: threadTs,
file_uploads: [
{
file: file.data,
filename: file.filename,
},
],
});
} else {
const messageParams: any = {
text,
channel: conversationId,
username,
icon_url: profilePicture,
blocks: blocks as Block[],
thread_ts: threadTs,
};
if (replyBroadcast) {
messageParams.reply_broadcast = replyBroadcast;
}
if (unfurlLinks === false) {
messageParams.unfurl_links = false;
}
return await client.chat.postMessage(messageParams);
}
};
type SlackSendMessageParams = {
token: string;
conversationId: string;
username?: string;
profilePicture?: string;
blocks?: unknown[] | Record<string, any>;
text?: string;
file?: ApFile;
threadTs?: string;
replyBroadcast?: boolean;
unfurlLinks?: boolean;
};
export function processMessageTimestamp(input: string) {
// Regular expression to match a URL containing the timestamp
const urlRegex = /\/p(\d+)(\d{6})$/;
// Check if the input is a URL
const urlMatch = input.match(urlRegex);
if (urlMatch) {
const timestamp = `${urlMatch[1]}.${urlMatch[2]}`;
return timestamp;
}
// Check if the input is already in the desired format
const timestampRegex = /^(\d+)\.(\d{6})$/;
const timestampMatch = input.match(timestampRegex);
if (timestampMatch) {
return input;
}
return undefined;
}
export function getFirstFiveOrAll(array: unknown[]) {
if (array.length <= 5) {
return array;
} else {
return array.slice(0, 5);
}
}
/**
* Parse a message text to extract command and arguments
*/
export function parseCommand(
text: string,
botUserId: string,
validCommands: string[]
): { command: string; args: string[] } | null {
if (!botUserId) {
return null;
}
// Check if the message mentions the bot
const mentionRegex = new RegExp(`<@${botUserId}>\\s+(.+)`, 's');
const mentionMatch = text.match(mentionRegex);
if (!mentionMatch) {
return null;
}
// Extract the text after the mention
const commandText = mentionMatch[1].trim();
// Split into command and arguments (first word is command, rest are args)
const parts = commandText.split(/\s+/);
const command = parts[0].toLowerCase();
const args = parts.slice(1);
// Check if it's a valid command
if (!validCommands.includes(command)) {
return null;
}
return {
command,
args,
};
}

View File

@@ -0,0 +1,66 @@
import { TriggerStrategy, createTrigger } from '@activepieces/pieces-framework';
import { slackAuth } from '../../';
import { WebClient } from '@slack/web-api';
const sampleData = {
type: 'channel_created',
channel: {
id: 'C024BE91L',
name: 'fun',
created: 1360782804,
creator: 'U024BE7LH',
},
};
export const channelCreated = createTrigger({
auth: slackAuth,
name: 'channel_created',
displayName: 'Channel created',
description: 'Triggers when a channel is created',
props: {},
type: TriggerStrategy.APP_WEBHOOK,
sampleData: sampleData,
onEnable: async (context) => {
// Older OAuth2 has team_id, newer has team.id
const teamId =
context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['channel_created'],
identifierValue: teamId,
});
},
onDisable: async (context) => {
// Ignored
},
test: async (context) => {
const client = new WebClient(context.auth.access_token);
const response = await client.conversations.list({
exclude_archived: true,
limit: 10,
types: 'public_channel,private_channel',
});
if (!response.channels) {
return [];
}
return response.channels.map((channel) => {
return {
type: 'channel_created',
channel: {
id: channel.id,
name: channel.name,
created: channel.created,
creator: channel.creator,
},
};
});
},
run: async (context) => {
const payloadBody = context.payload.body as PayloadBody;
return [payloadBody.event];
},
});
type PayloadBody = {
event: object;
};

View File

@@ -0,0 +1,101 @@
import {
Property,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { userId } from '../common/props';
import { slackAuth } from '../../';
import { parseCommand } from '../common/utils';
export const newCommandInDirectMessageTrigger = createTrigger({
auth: slackAuth,
name: 'new-command-in-direct-message',
displayName: 'New Command in Direct Message',
description:
'Triggers when a specific command is sent to the bot (e.g., @bot command arg1 arg2) via Direct Message.',
props: {
user: userId,
commands: Property.Array({
displayName: 'Commands',
description:
'List of valid commands that the bot should respond to (e.g., help, ocr, remind)',
required: true,
defaultValue: ['help'],
}),
ignoreBots: Property.Checkbox({
displayName: 'Ignore Bot Messages ?',
required: true,
defaultValue: true,
}),
ignoreSelfMessages: Property.Checkbox({
displayName: 'Ignore Message from Yourself ?',
required: true,
defaultValue: false,
}),
},
type: TriggerStrategy.APP_WEBHOOK,
sampleData: undefined,
onEnable: async (context) => {
// Older OAuth2 has team_id, newer has team.id
const teamId =
context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['message'],
identifierValue: teamId,
});
},
onDisable: async (context) => {
// Ignored
},
run: async (context) => {
const payloadBody = context.payload.body as PayloadBody;
const commands = (context.propsValue.commands as string[]) ?? [];
const user = context.propsValue.user as string;
const authUserId = context.auth.data['authed_user']?.id;
if (payloadBody.event.channel_type !== 'im') {
return [];
}
// Check for bot messages if configured to ignore them
if (
(context.propsValue.ignoreBots && payloadBody.event.bot_id) ||
(context.propsValue.ignoreSelfMessages && payloadBody.event.user === authUserId)
) {
return [];
}
// Check for mention and parse command
if (user && payloadBody.event.text) {
const parsedCommand = parseCommand(
payloadBody.event.text,
user,
commands
);
if (parsedCommand && commands.includes(parsedCommand.command)) {
// Return event with parsed command
return [
{
...payloadBody.event,
parsed_command: parsedCommand,
},
];
}
}
return [];
},
});
type PayloadBody = {
event: {
channel: string;
bot_id?: string;
text?: string;
channel_type: string;
user: string;
};
};

View File

@@ -0,0 +1,127 @@
import {
OAuth2PropertyValue,
Property,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { getChannels, multiSelectChannelInfo, userId } from '../common/props';
import { slackAuth } from '../../';
import { parseCommand } from '../common/utils';
export const newCommand = createTrigger({
auth: slackAuth,
name: 'new_command',
displayName: 'New Command in Channel',
description:
'Triggers when a specific command is sent to the bot (e.g., @bot command arg1 arg2)',
props: {
info: multiSelectChannelInfo,
user: userId,
commands: Property.Array({
displayName: 'Commands',
description:
'List of valid commands that the bot should respond to (e.g., help, ocr, remind)',
required: true,
defaultValue: ['help'],
}),
channels: Property.MultiSelectDropdown({
auth: slackAuth,
displayName: 'Channels',
description:
'If no channel is selected, the flow will be triggered for commands in all channels',
required: false,
refreshers: [],
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'connect slack account',
options: [],
};
}
const authentication = auth as OAuth2PropertyValue;
const accessToken = authentication['access_token'];
const channels = await getChannels(accessToken);
return {
disabled: false,
placeholder: 'Select channel',
options: channels,
};
},
}),
ignoreBots: Property.Checkbox({
displayName: 'Ignore Bot Messages ?',
required: true,
defaultValue: true,
}),
},
type: TriggerStrategy.APP_WEBHOOK,
sampleData: undefined,
onEnable: async (context) => {
// Older OAuth2 has team_id, newer has team.id
const teamId =
context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['message'],
identifierValue: teamId,
});
},
onDisable: async (context) => {
// Ignored
},
run: async (context) => {
const payloadBody = context.payload.body as PayloadBody;
const channels = (context.propsValue.channels as string[]) ?? [];
const commands = (context.propsValue.commands as string[]) ?? [];
const user = context.propsValue.user as string;
// check if it's channel message
if (!['channel','group'].includes(payloadBody.event.channel_type)) {
return [];
}
// Check if we should process this channel
if (
!(channels.length === 0 || channels.includes(payloadBody.event.channel))
) {
return [];
}
// Check for bot messages if configured to ignore them
if (context.propsValue.ignoreBots && payloadBody.event.bot_id) {
return [];
}
// Check for mention and parse command
if (user && payloadBody.event.text) {
const parsedCommand = parseCommand(
payloadBody.event.text,
user,
commands
);
if (parsedCommand && commands.includes(parsedCommand.command)) {
// Return event with parsed command
return [
{
...payloadBody.event,
parsed_command: parsedCommand,
},
];
}
}
return [];
},
});
type PayloadBody = {
event: {
channel: string;
bot_id?: string;
text?: string;
channel_type:string
};
};

View File

@@ -0,0 +1,63 @@
import { Property, TriggerStrategy, createTrigger } from '@activepieces/pieces-framework';
import { slackAuth } from '../../';
export const newDirectMessageTrigger = createTrigger({
auth: slackAuth,
name: 'new-direct-message',
displayName: 'New Direct Message',
description: 'Triggers when a message was posted in a direct message channel.',
props: {
ignoreBots: Property.Checkbox({
displayName: 'Ignore Bot Messages ?',
required: true,
defaultValue: false,
}),
ignoreSelfMessages: Property.Checkbox({
displayName: 'Ignore Message from Yourself ?',
required: true,
defaultValue: false,
}),
},
type: TriggerStrategy.APP_WEBHOOK,
sampleData: undefined,
onEnable: async (context) => {
// Older OAuth2 has team_id, newer has team.id
const teamId = context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['message'],
identifierValue: teamId,
});
},
onDisable: async (context) => {
// Ignored
},
async run(context) {
const payloadBody = context.payload.body as PayloadBody;
const userId = context.auth.data['authed_user']?.id;
if (payloadBody.event.channel_type !== 'im') {
return [];
}
// check for bot messages
if (
(context.propsValue.ignoreBots && payloadBody.event.bot_id) ||
(context.propsValue.ignoreSelfMessages && payloadBody.event.user === userId)
) {
return [];
}
return [payloadBody.event];
},
});
type PayloadBody = {
event: {
channel: string;
bot_id?: string;
user: string;
channel_type: string;
};
};

View File

@@ -0,0 +1,80 @@
import {
Property,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { slackAuth } from '../../';
import { userId } from '../common/props';
export const newMentionInDirectMessageTrigger = createTrigger({
auth: slackAuth,
name: 'new-mention-in-direct-message',
displayName: 'New Mention in Direct Message',
description:
'Triggers when a username is mentioned in a direct message channel.',
props: {
user: userId,
ignoreBots: Property.Checkbox({
displayName: 'Ignore Bot Messages ?',
required: true,
defaultValue: false,
}),
ignoreSelfMessages: Property.Checkbox({
displayName: 'Ignore Message from Yourself ?',
required: true,
defaultValue: false,
}),
},
type: TriggerStrategy.APP_WEBHOOK,
sampleData: undefined,
onEnable: async (context) => {
// Older OAuth2 has team_id, newer has team.id
const teamId =
context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['message'],
identifierValue: teamId,
});
},
onDisable: async (context) => {
// Ignored
},
async run(context) {
const payloadBody = context.payload.body as PayloadBody;
const userId = context.auth.data['authed_user']?.id;
if (payloadBody.event.channel_type !== 'im') {
return [];
}
// check for bot messages
if (
(context.propsValue.ignoreBots && payloadBody.event.bot_id) ||
(context.propsValue.ignoreSelfMessages &&
payloadBody.event.user === userId)
) {
return [];
}
//check for mention
if (
context.propsValue.user &&
payloadBody.event.text?.includes(`<@${context.propsValue.user}>`)
) {
return [payloadBody.event];
}
return [];
},
});
type PayloadBody = {
event: {
channel: string;
bot_id?: string;
user: string;
channel_type: string;
text?: string;
};
};

View File

@@ -0,0 +1,99 @@
import {
OAuth2PropertyValue,
Property,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { getChannels, multiSelectChannelInfo, userId } from '../common/props';
import { slackAuth } from '../../';
export const newMention = createTrigger({
auth: slackAuth,
name: 'new_mention',
displayName: 'New Mention in Channel',
description: 'Triggers when a username is mentioned.',
props: {
info: multiSelectChannelInfo,
user: userId,
channels: Property.MultiSelectDropdown({
auth: slackAuth,
displayName: 'Channels',
description:
'If no channel is selected, the flow will be triggered for username mentions in all channels',
required: false,
refreshers: [],
async options({ auth }) {
if (!auth) {
return {
disabled: true,
placeholder: 'connect slack account',
options: [],
};
}
const authentication = auth as OAuth2PropertyValue;
const accessToken = authentication['access_token'];
const channels = await getChannels(accessToken);
return {
disabled: false,
placeholder: 'Select channel',
options: channels,
};
},
}),
ignoreBots: Property.Checkbox({
displayName: 'Ignore Bot Messages ?',
required: true,
defaultValue: false,
}),
},
type: TriggerStrategy.APP_WEBHOOK,
sampleData: undefined,
onEnable: async (context) => {
// Older OAuth2 has team_id, newer has team.id
const teamId =
context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['message'],
identifierValue: teamId,
});
},
onDisable: async (context) => {
// Ignored
},
run: async (context) => {
const payloadBody = context.payload.body as PayloadBody;
const channels = (context.propsValue.channels as string[]) ?? [];
// check if it's channel message
if (!['channel','group'].includes(payloadBody.event.channel_type)) {
return [];
}
if (channels.length === 0 || channels.includes(payloadBody.event.channel)) {
// check for bot messages
if (context.propsValue.ignoreBots && payloadBody.event.bot_id) {
return [];
}
// check for mention
if (
context.propsValue.user &&
payloadBody.event.text?.includes(`<@${context.propsValue.user}>`)
) {
return [payloadBody.event];
}
}
return [];
},
});
type PayloadBody = {
event: {
channel: string;
bot_id?: string;
text?: string;
channel_type:string
};
};

View File

@@ -0,0 +1,64 @@
import { Property, TriggerStrategy, createTrigger } from '@activepieces/pieces-framework';
import { singleSelectChannelInfo, slackChannel } from '../common/props';
import { slackAuth } from '../../';
import { WebClient } from '@slack/web-api';
import { isNil } from '@activepieces/shared';
import { getFirstFiveOrAll } from '../common/utils';
export const newMessageInChannelTrigger = createTrigger({
auth: slackAuth,
name: 'new-message-in-channel',
displayName: 'New Message Posted to Channel',
description: 'Triggers when a new message is posted to a specific #channel you choose.',
props: {
info: singleSelectChannelInfo,
channel: slackChannel(true),
ignoreBots: Property.Checkbox({
displayName: 'Ignore Bot Messages ?',
required: true,
defaultValue: false,
}),
},
type: TriggerStrategy.APP_WEBHOOK,
sampleData: undefined,
async onEnable(context) {
// Older OAuth2 has team_id, newer has team.id
const teamId = context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['message'],
identifierValue: teamId,
});
},
async onDisable(context) {
// Ignored
},
async run(context) {
const payloadBody = context.payload.body as PayloadBody;
// check if it's channel message
if (!['channel','group'].includes(payloadBody.event.channel_type)) {
return [];
}
if (payloadBody.event.channel === context.propsValue.channel) {
// check for bot messages
if (context.propsValue.ignoreBots && payloadBody.event.bot_id) {
return [];
}
return [payloadBody.event];
}
return [];
},
});
type PayloadBody = {
event: {
channel: string;
bot_id?: string;
channel_type:string
};
};

View File

@@ -0,0 +1,52 @@
import { Property, TriggerStrategy, createTrigger } from '@activepieces/pieces-framework';
import { slackAuth } from '../../';
export const newMessageTrigger = createTrigger({
auth: slackAuth,
name: 'new-message',
displayName: 'New Public Message Posted Anywhere',
description: 'Triggers when a new message is posted to any channel.',
props: {
ignoreBots: Property.Checkbox({
displayName: 'Ignore Bot Messages ?',
required: true,
defaultValue: false,
}),
},
type: TriggerStrategy.APP_WEBHOOK,
sampleData: undefined,
onEnable: async (context) => {
// Older OAuth2 has team_id, newer has team.id
const teamId = context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['message'],
identifierValue: teamId,
});
},
onDisable: async (context) => {
// Ignored
},
run: async (context) => {
const payloadBody = context.payload.body as PayloadBody;
// check if it's channel message
if (!['channel','group'].includes(payloadBody.event.channel_type)) {
return [];
}
// check for bot messages
if (context.propsValue.ignoreBots && payloadBody.event.bot_id) {
return [];
}
return [payloadBody.event];
},
});
type PayloadBody = {
event: {
channel: string;
bot_id?: string;
channel_type:string
};
};

View File

@@ -0,0 +1,64 @@
import {
Property,
TriggerStrategy,
createTrigger,
} from '@activepieces/pieces-framework';
import { slackAuth } from '../../';
import { slackChannel } from '../common/props';
import { WebClient } from '@slack/web-api';
export const newReactionAdded = createTrigger({
auth: slackAuth,
name: 'new_reaction_added',
displayName: 'New Reaction',
description: 'Triggers when a new reaction is added to a message',
props: {
emojis: Property.Array({
displayName: 'Emojis (E.g fire, smile)',
description: 'Select emojis to trigger on',
required: false,
}),
channel: slackChannel(false),
},
type: TriggerStrategy.APP_WEBHOOK,
sampleData: undefined,
onEnable: async (context) => {
// Older OAuth2 has team_id, newer has team.id
const teamId =
context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['reaction_added'],
identifierValue: teamId,
});
},
onDisable: async (context) => {
// Ignored
},
run: async (context) => {
const payloadBody = context.payload.body as PayloadBody;
if (context.propsValue.emojis) {
if (!context.propsValue.emojis.includes(payloadBody.event.reaction)) {
return [];
}
}
if (context.propsValue.channel) {
if (payloadBody.event.item['channel'] !== context.propsValue.channel) {
return [];
}
}
return [payloadBody.event];
},
});
type PayloadBody = {
event: {
reaction: string;
item: {
channel: string;
};
};
};

View File

@@ -0,0 +1,44 @@
import { TriggerStrategy, createTrigger } from '@activepieces/pieces-framework';
import { slackAuth } from '../../';
export const newSavedMessageTrigger = createTrigger({
auth: slackAuth,
name: 'new-saved-message',
displayName: 'New Saved Message',
description: 'Triggers when you save a message.',
props: {},
type: TriggerStrategy.APP_WEBHOOK,
sampleData: undefined,
onEnable: async (context) => {
// Older OAuth2 has team_id, newer has team.id
const teamId = context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['star_added'],
identifierValue: teamId,
});
},
onDisable: async (context) => {
// Ignored
},
run: async (context) => {
const payloadBody = context.payload.body as PayloadBody;
// check if it's saved message
if (payloadBody.event.type === 'star_added' && payloadBody.event.item.type ==='message') {
return [payloadBody.event.item];
}
return [];
},
});
type PayloadBody = {
event: {
type: string;
event_ts: string;
item:{
type:string
}
};
};

View File

@@ -0,0 +1,62 @@
import { TriggerStrategy, createTrigger } from '@activepieces/pieces-framework';
import { slackAuth } from '../../';
import { WebClient } from '@slack/web-api';
const sampleData = {
id: 'heart',
image: 'https://emoji.slack-edge.com/T06BTHUEFFF/heart/84a171ae62daacc7.jpg',
};
export const newTeamCustomEmojiTrigger = createTrigger({
auth: slackAuth,
name: 'new-team-custom-emoji',
displayName: 'New Team Custom Emoji',
description: 'Triggers when a custom emoji has been added to a team.',
props: {},
type: TriggerStrategy.APP_WEBHOOK,
sampleData,
onEnable: async (context) => {
// Older OAuth2 has team_id, newer has team.id
const teamId = context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['emoji_changed'],
identifierValue: teamId,
});
},
onDisable: async (context) => {
// Ignored
},
test: async (context) => {
const client = new WebClient(context.auth.access_token);
const response = await client.emoji.list();
if (!response.emoji) return [sampleData];
return Object.entries(response.emoji).map(([id, image]) => ({
id,
image,
}));
},
run: async (context) => {
const payloadBody = context.payload.body as PayloadBody;
// check if it's emoji message
if (payloadBody.event.type !== 'emoji_changed' && payloadBody.event.subtype !== 'add') {
return [];
}
return [{ id: payloadBody.event.name, image: payloadBody.event.value }];
},
});
type PayloadBody = {
event: {
type: string;
subtype: string;
name: string;
value: string;
};
};

View File

@@ -0,0 +1,103 @@
import { TriggerStrategy, createTrigger } from '@activepieces/pieces-framework';
import { slackAuth } from '../../';
import { WebClient } from '@slack/web-api';
const sampleData = {
id: 'USLACKBOT',
team_id: 'T06BTHUEFFF',
name: 'slackbot',
deleted: false,
color: '757575',
real_name: 'Slackbot',
tz: 'America/Los_Angeles',
tz_label: 'Pacific Daylight Time',
tz_offset: -25200,
profile: {
title: '',
phone: '',
skype: '',
real_name: 'Slackbot',
real_name_normalized: 'Slackbot',
display_name: 'Slackbot',
display_name_normalized: 'Slackbot',
fields: {},
status_text: '',
status_emoji: '',
status_emoji_display_info: [],
status_expiration: 0,
avatar_hash: 'sv41d8cd98f0',
always_active: true,
first_name: 'slackbot',
last_name: '',
image_24: 'https://a.slack-edge.com/80588/img/slackbot_24.png',
image_32: 'https://a.slack-edge.com/80588/img/slackbot_32.png',
image_48: 'https://a.slack-edge.com/80588/img/slackbot_48.png',
image_72: 'https://a.slack-edge.com/80588/img/slackbot_72.png',
image_192: 'https://a.slack-edge.com/80588/marketing/img/avatars/slackbot/avatar-slackbot.png',
image_512: 'https://a.slack-edge.com/80588/img/slackbot_512.png',
status_text_canonical: '',
team: 'T06BTHUEFFF',
},
is_admin: false,
is_owner: false,
is_primary_owner: false,
is_restricted: false,
is_ultra_restricted: false,
is_bot: false,
is_app_user: false,
updated: 0,
is_email_confirmed: false,
who_can_share_contact_card: 'EVERYONE',
};
export const newUserTrigger = createTrigger({
auth: slackAuth,
name: 'new-user',
displayName: 'New User',
description: 'Triggers when a new user is created / first joins your org.',
props: {},
type: TriggerStrategy.APP_WEBHOOK,
sampleData,
onEnable: async (context) => {
// Older OAuth2 has team_id, newer has team.id
const teamId = context.auth.data['team_id'] ?? context.auth.data['team']['id'];
context.app.createListeners({
events: ['team_join'],
identifierValue: teamId,
});
},
onDisable: async (context) => {
// Ignored
},
test: async (context) => {
const client = new WebClient(context.auth.access_token);
const response = await client.users.list({limit:10});
if (!response.members) return [sampleData];
return response.members;
},
run: async (context) => {
const payloadBody = context.payload.body as PayloadBody;
// check if it's emoji message
if (payloadBody.event.type !== 'team_join') {
return [];
}
return [payloadBody.event.user];
},
});
type PayloadBody = {
event: {
type: string;
event_ts:string,
user:{
id:string
}
};
};