feat(slack): implement channel welcome message functionality (#23699)
https://github.com/user-attachments/assets/a77aa941-da48-4c30-8e14-587516c19ac4 Added a new feature that allows the bot to introduce itself when added to a Slack channel. This includes a welcome message and a detailed thread reply outlining its capabilities. The implementation includes new utility functions for handling the welcome event, managing welcome state, and posting messages. Updated relevant logic functions to support this feature, ensuring the bot can provide a seamless introduction to users in new channels. - Introduced `slack-channel-welcome` logic function. - Added constants for welcome message text. - Implemented event parsing and handling for `member_joined_channel`. - Updated `slack-events-resolver` to route welcome events appropriately.
This commit is contained in:
@@ -17,6 +17,8 @@ Anyone who can message the bot acts with the **Slack Assistant** role, which by
|
||||
|
||||
One Slack workspace answers into one Twenty workspace.
|
||||
|
||||
When the bot is added to a channel it introduces itself once, with a short message in the channel and the details (what to ask it, what it reads, and the shared-role caveat above) in a thread reply. It needs the `member_joined_channel` subscription, so leave that one off if you want the bot to arrive quietly.
|
||||
|
||||
## 🧰 The workflow steps
|
||||
|
||||
| Step | Slack API |
|
||||
|
||||
@@ -60,6 +60,7 @@ The assistant reuses the same Slack connection — no second bot identity.
|
||||
- `message.im` — direct messages to the bot
|
||||
- `message.channels` — replies in public-channel threads, for un-mentioned follow-ups
|
||||
- `message.groups` — same, for private channels the bot is in
|
||||
- `member_joined_channel` — optional; lets the bot introduce itself when it is added to a channel
|
||||
|
||||
Invite the bot to any channel where it should follow threads. Slack may ask you to reinstall after changing subscriptions.
|
||||
|
||||
@@ -70,6 +71,7 @@ The assistant reuses the same Slack connection — no second bot identity.
|
||||
## Behaviour notes
|
||||
|
||||
- **Thread memory.** After a successful reply the bot stays active in that thread, so follow-ups need no mention. Channel threads stay active for 24 hours after the last reply (each reply renews it); DM threads never expire.
|
||||
- **Channel welcome.** With `member_joined_channel` subscribed, the bot posts a short introduction the first time it is added to a channel, with the details (what to ask it, what it reads, and the shared-role caveat from step 4 above) in a thread reply so the channel itself stays quiet. It fires once per channel for 30 days, and only for the bot's own join — humans joining afterwards trigger nothing. Skip the subscription if you would rather it arrived silently.
|
||||
- **One Slack workspace per Twenty workspace.** Connecting Slack claims that Slack team for the connecting Twenty workspace. On the same server, a second Twenty workspace connecting the same Slack team is rejected. The claim is not released on disconnect yet, so moving a Slack workspace needs a server admin.
|
||||
|
||||
## Workflow field names (for step authors)
|
||||
|
||||
@@ -49,6 +49,9 @@ export const SLACK_EVENTS_ROUTE_UNIVERSAL_IDENTIFIER =
|
||||
export const SLACK_EVENTS_ENQUEUE_UNIVERSAL_IDENTIFIER =
|
||||
'8f2e1d3c-4b5a-4c6d-9e7f-0a1b2c3d4e5f';
|
||||
|
||||
export const SLACK_CHANNEL_WELCOME_UNIVERSAL_IDENTIFIER =
|
||||
'c5e8b13a-7f42-4d90-8a6b-1e3c9d052f47';
|
||||
|
||||
export const SLACK_ASSISTANT_WORKER_UNIVERSAL_IDENTIFIER =
|
||||
'4b92a49f-d674-46ea-a3d9-e8d658ae3a17';
|
||||
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export const SLACK_CHANNEL_WELCOME_TEXT = [
|
||||
":wave: Hi! I'm Twenty, your CRM in this channel.",
|
||||
"Mention me here or send me a DM and I'll answer in the thread: pipeline questions, company and contact lookups, creating and updating records, capturing notes and tasks.",
|
||||
'Details on how I work and what I can see are in the thread. Ask me anything.',
|
||||
].join('\n\n');
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
export const SLACK_CHANNEL_WELCOME_THREAD_TEXT = [
|
||||
'**A few things to ask me**',
|
||||
[
|
||||
'- **Look things up**: "how many open opportunities are in the pipeline?", "who owns the ACME account?"',
|
||||
'- **Create and update records**: "add ACME as a company", "move the ACME deal to Proposal"',
|
||||
'- **Capture notes and tasks**: "note that ACME wants a security review", "task Alice with sending the pricing deck"',
|
||||
].join('\n'),
|
||||
'**How I work with your data**',
|
||||
[
|
||||
"- I only act when you mention me, or in a thread I've already replied in. Channel threads stay open to me for 24 hours after my last reply",
|
||||
'- When you mention me I read recent messages in that thread for context',
|
||||
"- I can read, create, update and archive people, companies, opportunities, notes and tasks. I can't permanently delete anything and I can't change workspace settings",
|
||||
'- Everyone in this channel talks to me through the same **Slack Assistant** role, so anyone who can message me sees whatever that role can see',
|
||||
].join('\n'),
|
||||
].join('\n\n');
|
||||
+54
@@ -108,6 +108,21 @@ describe('slackPostMessageHandler', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should treat a runtime non-string parent timestamp as no thread without throwing', async () => {
|
||||
postMessageMock.mockResolvedValue({ ts: '1700000000.000450' });
|
||||
|
||||
const result = await slackPostMessageHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageText: 'standalone',
|
||||
parentMessageTimestamp: 1700000000 as unknown as string,
|
||||
});
|
||||
|
||||
expect(postMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ thread_ts: undefined }),
|
||||
);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should return a failure result when the Slack API throws', async () => {
|
||||
postMessageMock.mockRejectedValue(new Error('channel_not_found'));
|
||||
|
||||
@@ -122,4 +137,43 @@ describe('slackPostMessageHandler', () => {
|
||||
error: 'channel_not_found',
|
||||
});
|
||||
});
|
||||
|
||||
it('should retry as plain text when the workspace rejects markdown_text', async () => {
|
||||
postMessageMock
|
||||
.mockRejectedValueOnce(
|
||||
Object.assign(new Error('invalid_arguments'), {
|
||||
data: { error: 'invalid_arguments' },
|
||||
}),
|
||||
)
|
||||
.mockResolvedValueOnce({ ts: '1700000000.000500', channel: CHANNEL_ID });
|
||||
|
||||
const result = await slackPostMessageHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageText: '**hello**',
|
||||
messageFormat: 'markdown',
|
||||
});
|
||||
|
||||
expect(postMessageMock).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ text: '**hello**', mrkdwn: false }),
|
||||
);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should not retry when a markdown post fails for an unrelated reason', async () => {
|
||||
postMessageMock.mockRejectedValue(
|
||||
Object.assign(new Error('channel_not_found'), {
|
||||
data: { error: 'channel_not_found' },
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await slackPostMessageHandler({
|
||||
slackChannelId: CHANNEL_ID,
|
||||
messageText: '**hello**',
|
||||
messageFormat: 'markdown',
|
||||
});
|
||||
|
||||
expect(postMessageMock).toHaveBeenCalledTimes(1);
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+2
-32
@@ -1,8 +1,7 @@
|
||||
import { type SlackPostMessageInput } from 'src/logic-functions/types/slack-post-message-input.type';
|
||||
import { type SlackToolResult } from 'src/logic-functions/types/slack-tool-result.type';
|
||||
import { getSlackChatMessageBodyFields } from 'src/logic-functions/utils/get-slack-chat-message-body-fields';
|
||||
import { getSlackClient } from 'src/logic-functions/utils/get-slack-client';
|
||||
import { slackToolFailure } from 'src/logic-functions/utils/slack-tool-failure';
|
||||
import { postSlackMessage } from 'src/logic-functions/utils/post-slack-message';
|
||||
|
||||
export const slackPostMessageHandler = async (
|
||||
parameters: SlackPostMessageInput,
|
||||
@@ -17,34 +16,5 @@ export const slackPostMessageHandler = async (
|
||||
};
|
||||
}
|
||||
|
||||
const { client } = slackClientResult;
|
||||
|
||||
const parentTimestamp = parameters.parentMessageTimestamp;
|
||||
|
||||
try {
|
||||
const bodyFields = getSlackChatMessageBodyFields(
|
||||
parameters.messageText,
|
||||
parameters.messageFormat,
|
||||
);
|
||||
|
||||
const data = await client.chat.postMessage({
|
||||
channel: parameters.slackChannelId,
|
||||
thread_ts:
|
||||
parentTimestamp != null && parentTimestamp.trim().length > 0
|
||||
? parentTimestamp.trim()
|
||||
: undefined,
|
||||
...bodyFields,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: data.ts
|
||||
? `Message posted to Slack (ts=${data.ts}).`
|
||||
: 'Message posted to Slack.',
|
||||
slackTs: data.ts,
|
||||
channel: data.channel,
|
||||
};
|
||||
} catch (error) {
|
||||
return slackToolFailure('Failed to post Slack message', error);
|
||||
}
|
||||
return await postSlackMessage(slackClientResult.client, parameters);
|
||||
};
|
||||
|
||||
+22
-38
@@ -2,8 +2,7 @@ import { type SlackToolResult } from 'src/logic-functions/types/slack-tool-resul
|
||||
import { type SlackUpdateMessageInput } from 'src/logic-functions/types/slack-update-message-input.type';
|
||||
import { getSlackChatMessageBodyFields } from 'src/logic-functions/utils/get-slack-chat-message-body-fields';
|
||||
import { getSlackClient } from 'src/logic-functions/utils/get-slack-client';
|
||||
import { isSlackMarkdownFormatError } from 'src/logic-functions/utils/is-slack-markdown-format-error';
|
||||
import { slackToolFailure } from 'src/logic-functions/utils/slack-tool-failure';
|
||||
import { sendSlackMessageWithMarkdownFallback } from 'src/logic-functions/utils/send-slack-message-with-markdown-fallback';
|
||||
|
||||
export const slackUpdateMessageHandler = async (
|
||||
parameters: SlackUpdateMessageInput,
|
||||
@@ -20,42 +19,27 @@ export const slackUpdateMessageHandler = async (
|
||||
|
||||
const { client } = slackClientResult;
|
||||
|
||||
const updateWithFormat = async (
|
||||
messageFormat: SlackUpdateMessageInput['messageFormat'],
|
||||
): Promise<SlackToolResult> => {
|
||||
const bodyFields = getSlackChatMessageBodyFields(
|
||||
parameters.newMessageText,
|
||||
messageFormat,
|
||||
);
|
||||
return await sendSlackMessageWithMarkdownFallback({
|
||||
messageFormat: parameters.messageFormat,
|
||||
failureMessage: 'Failed to update Slack message',
|
||||
sendMessage: async (messageFormat) => {
|
||||
const bodyFields = getSlackChatMessageBodyFields(
|
||||
parameters.newMessageText,
|
||||
messageFormat,
|
||||
);
|
||||
|
||||
const data = await client.chat.update({
|
||||
channel: parameters.slackChannelId,
|
||||
ts: parameters.messageTimestamp,
|
||||
...bodyFields,
|
||||
});
|
||||
const data = await client.chat.update({
|
||||
channel: parameters.slackChannelId,
|
||||
ts: parameters.messageTimestamp,
|
||||
...bodyFields,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Slack message updated.',
|
||||
slackTs: data.ts,
|
||||
channel: parameters.slackChannelId,
|
||||
};
|
||||
};
|
||||
|
||||
try {
|
||||
return await updateWithFormat(parameters.messageFormat);
|
||||
} catch (error) {
|
||||
if (
|
||||
parameters.messageFormat !== 'markdown' ||
|
||||
!isSlackMarkdownFormatError(error)
|
||||
) {
|
||||
return slackToolFailure('Failed to update Slack message', error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await updateWithFormat('plain');
|
||||
} catch (error) {
|
||||
return slackToolFailure('Failed to update Slack message', error);
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: 'Slack message updated.',
|
||||
slackTs: data.ts,
|
||||
channel: parameters.slackChannelId,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineLogicFunction } from 'twenty-sdk/define';
|
||||
|
||||
import { SLACK_CHANNEL_WELCOME_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
import { postSlackChannelWelcome } from 'src/logic-functions/utils/post-slack-channel-welcome';
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: SLACK_CHANNEL_WELCOME_UNIVERSAL_IDENTIFIER,
|
||||
name: 'slack-channel-welcome',
|
||||
description:
|
||||
'Runs in the resolved workspace: posts a one-off introduction when the bot itself is added to a Slack channel.',
|
||||
timeoutSeconds: 15,
|
||||
handler: postSlackChannelWelcome,
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define';
|
||||
import { Response } from 'twenty-sdk/logic-function';
|
||||
|
||||
import {
|
||||
SLACK_CHANNEL_WELCOME_UNIVERSAL_IDENTIFIER,
|
||||
SLACK_EVENTS_ENQUEUE_UNIVERSAL_IDENTIFIER,
|
||||
SLACK_EVENTS_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/constants/universal-identifiers';
|
||||
@@ -58,7 +59,9 @@ export const slackEventsResolverHandler = async (
|
||||
return {
|
||||
workspaceId: await resolveTargetWorkspaceId(body),
|
||||
targetLogicFunctionUniversalIdentifier:
|
||||
SLACK_EVENTS_ENQUEUE_UNIVERSAL_IDENTIFIER,
|
||||
body.event?.type === 'member_joined_channel'
|
||||
? SLACK_CHANNEL_WELCOME_UNIVERSAL_IDENTIFIER
|
||||
: SLACK_EVENTS_ENQUEUE_UNIVERSAL_IDENTIFIER,
|
||||
payload: body,
|
||||
};
|
||||
};
|
||||
@@ -67,7 +70,7 @@ export default defineLogicFunction({
|
||||
universalIdentifier: SLACK_EVENTS_ROUTE_UNIVERSAL_IDENTIFIER,
|
||||
name: 'slack-events-resolver',
|
||||
description:
|
||||
'Receives Slack Events API callbacks, verifies the request signature in the owner workspace, answers the url_verification handshake, and resolves the target workspace + enqueue function for the assistant.',
|
||||
'Receives Slack Events API callbacks, verifies the request signature in the owner workspace, answers the url_verification handshake, and resolves the target workspace plus the function that handles the event (assistant enqueue, or the channel welcome on member_joined_channel).',
|
||||
timeoutSeconds: 15,
|
||||
handler: slackEventsResolverHandler,
|
||||
serverRouteTriggerSettings: {
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
export type SlackChannelWelcome = {
|
||||
expiresAt: number;
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { isNumber } from '@sniptt/guards';
|
||||
import { kv } from 'twenty-sdk/logic-function';
|
||||
|
||||
import { type SlackChannelWelcome } from 'src/logic-functions/types/slack-channel-welcome.type';
|
||||
import { getSlackChannelWelcomeKvKey } from 'src/logic-functions/utils/get-slack-channel-welcome-kv-key';
|
||||
|
||||
const SLACK_CHANNEL_WELCOME_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export const claimSlackChannelWelcome = async (
|
||||
channelId: string,
|
||||
): Promise<boolean> => {
|
||||
const key = getSlackChannelWelcomeKvKey(channelId);
|
||||
const existingWelcome = await kv.get<SlackChannelWelcome>(key);
|
||||
|
||||
if (
|
||||
existingWelcome !== null &&
|
||||
isNumber(existingWelcome.expiresAt) &&
|
||||
existingWelcome.expiresAt > Date.now()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await kv.set(key, {
|
||||
expiresAt: Date.now() + SLACK_CHANNEL_WELCOME_TTL_MS,
|
||||
} satisfies SlackChannelWelcome);
|
||||
|
||||
return true;
|
||||
};
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
export const getSlackChannelWelcomeKvKey = (channelId: string): string =>
|
||||
`slack-channel-welcome:${channelId}`;
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { type SlackEventsRequestBody } from 'src/logic-functions/types/slack-events-request-body.type';
|
||||
|
||||
type ParsedSlackChannelWelcomeEvent =
|
||||
| { channelJoin: { slackChannelId: string; slackUserId: string } }
|
||||
| { channelJoin: null; skipReason: string };
|
||||
|
||||
export const parseSlackChannelWelcomeEvent = (
|
||||
body: SlackEventsRequestBody,
|
||||
): ParsedSlackChannelWelcomeEvent => {
|
||||
if (body.type !== 'event_callback') {
|
||||
return {
|
||||
channelJoin: null,
|
||||
skipReason: `Unhandled body type: ${body.type}`,
|
||||
};
|
||||
}
|
||||
|
||||
const event = body.event;
|
||||
|
||||
if (!event) {
|
||||
return { channelJoin: null, skipReason: 'Missing event payload' };
|
||||
}
|
||||
|
||||
if (event.type !== 'member_joined_channel') {
|
||||
return {
|
||||
channelJoin: null,
|
||||
skipReason: `Unhandled event type: ${event.type}`,
|
||||
};
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(event.channel) || !isNonEmptyString(event.user)) {
|
||||
return {
|
||||
channelJoin: null,
|
||||
skipReason: 'Event is missing required fields',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
channelJoin: { slackChannelId: event.channel, slackUserId: event.user },
|
||||
};
|
||||
};
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { SLACK_CHANNEL_WELCOME_TEXT } from 'src/logic-functions/constants/slack-channel-welcome-text';
|
||||
import { SLACK_CHANNEL_WELCOME_THREAD_TEXT } from 'src/logic-functions/constants/slack-channel-welcome-thread-text';
|
||||
import { type SlackEventsRequestBody } from 'src/logic-functions/types/slack-events-request-body.type';
|
||||
import { claimSlackChannelWelcome } from 'src/logic-functions/utils/claim-slack-channel-welcome';
|
||||
import { getSlackClient } from 'src/logic-functions/utils/get-slack-client';
|
||||
import { parseSlackChannelWelcomeEvent } from 'src/logic-functions/utils/parse-slack-channel-welcome-event';
|
||||
import { postSlackMessage } from 'src/logic-functions/utils/post-slack-message';
|
||||
import { releaseSlackChannelWelcome } from 'src/logic-functions/utils/release-slack-channel-welcome';
|
||||
|
||||
type SlackChannelWelcomeResult = { ok: boolean; skipped?: string };
|
||||
|
||||
export const postSlackChannelWelcome = async (
|
||||
body: SlackEventsRequestBody,
|
||||
): Promise<SlackChannelWelcomeResult> => {
|
||||
const parsed = parseSlackChannelWelcomeEvent(body);
|
||||
|
||||
if (parsed.channelJoin === null) {
|
||||
return { ok: true, skipped: parsed.skipReason };
|
||||
}
|
||||
|
||||
const { slackChannelId, slackUserId } = parsed.channelJoin;
|
||||
|
||||
const slackClientResult = await getSlackClient();
|
||||
|
||||
if (!slackClientResult.success) {
|
||||
throw new Error(slackClientResult.error);
|
||||
}
|
||||
|
||||
const { client } = slackClientResult;
|
||||
|
||||
const authResult = await client.auth.test();
|
||||
|
||||
if (authResult.user_id !== slackUserId) {
|
||||
return { ok: true, skipped: 'Someone other than the bot joined' };
|
||||
}
|
||||
|
||||
const isFirstWelcome = await claimSlackChannelWelcome(slackChannelId);
|
||||
|
||||
if (!isFirstWelcome) {
|
||||
return { ok: true, skipped: 'Channel was already welcomed' };
|
||||
}
|
||||
|
||||
const channelMessageResult = await postSlackMessage(client, {
|
||||
slackChannelId,
|
||||
messageText: SLACK_CHANNEL_WELCOME_TEXT,
|
||||
messageFormat: 'markdown',
|
||||
});
|
||||
|
||||
if (
|
||||
!channelMessageResult.success ||
|
||||
!isNonEmptyString(channelMessageResult.slackTs)
|
||||
) {
|
||||
await releaseSlackChannelWelcome(slackChannelId);
|
||||
|
||||
throw new Error(
|
||||
`Failed to post the Slack welcome in channel ${slackChannelId}: ${channelMessageResult.error ?? channelMessageResult.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
const threadMessageResult = await postSlackMessage(client, {
|
||||
slackChannelId,
|
||||
messageText: SLACK_CHANNEL_WELCOME_THREAD_TEXT,
|
||||
parentMessageTimestamp: channelMessageResult.slackTs,
|
||||
messageFormat: 'markdown',
|
||||
});
|
||||
|
||||
if (!threadMessageResult.success) {
|
||||
throw new Error(
|
||||
`Failed to post the Slack welcome thread reply in channel ${slackChannelId}: ${threadMessageResult.error ?? threadMessageResult.message}`,
|
||||
);
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type WebClient } from '@slack/web-api';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { type SlackPostMessageInput } from 'src/logic-functions/types/slack-post-message-input.type';
|
||||
import { type SlackToolResult } from 'src/logic-functions/types/slack-tool-result.type';
|
||||
import { getSlackChatMessageBodyFields } from 'src/logic-functions/utils/get-slack-chat-message-body-fields';
|
||||
import { sendSlackMessageWithMarkdownFallback } from 'src/logic-functions/utils/send-slack-message-with-markdown-fallback';
|
||||
|
||||
export const postSlackMessage = async (
|
||||
client: WebClient,
|
||||
parameters: SlackPostMessageInput,
|
||||
): Promise<SlackToolResult> => {
|
||||
const parentTimestamp = isNonEmptyString(parameters.parentMessageTimestamp)
|
||||
? parameters.parentMessageTimestamp.trim() || undefined
|
||||
: undefined;
|
||||
|
||||
return await sendSlackMessageWithMarkdownFallback({
|
||||
messageFormat: parameters.messageFormat,
|
||||
failureMessage: 'Failed to post Slack message',
|
||||
sendMessage: async (messageFormat) => {
|
||||
const bodyFields = getSlackChatMessageBodyFields(
|
||||
parameters.messageText,
|
||||
messageFormat,
|
||||
);
|
||||
|
||||
const data = await client.chat.postMessage({
|
||||
channel: parameters.slackChannelId,
|
||||
thread_ts: parentTimestamp,
|
||||
...bodyFields,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: data.ts
|
||||
? `Message posted to Slack (ts=${data.ts}).`
|
||||
: 'Message posted to Slack.',
|
||||
slackTs: data.ts,
|
||||
channel: data.channel,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { kv } from 'twenty-sdk/logic-function';
|
||||
|
||||
import { getSlackChannelWelcomeKvKey } from 'src/logic-functions/utils/get-slack-channel-welcome-kv-key';
|
||||
|
||||
export const releaseSlackChannelWelcome = async (
|
||||
channelId: string,
|
||||
): Promise<void> => {
|
||||
await kv.delete(getSlackChannelWelcomeKvKey(channelId));
|
||||
};
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import { type SlackMessageBodyFormat } from 'src/logic-functions/types/slack-message-body-format.type';
|
||||
import { type SlackToolResult } from 'src/logic-functions/types/slack-tool-result.type';
|
||||
import { isSlackMarkdownFormatError } from 'src/logic-functions/utils/is-slack-markdown-format-error';
|
||||
import { slackToolFailure } from 'src/logic-functions/utils/slack-tool-failure';
|
||||
|
||||
type SendSlackMessageWithMarkdownFallbackParams = {
|
||||
messageFormat: SlackMessageBodyFormat | undefined;
|
||||
failureMessage: string;
|
||||
sendMessage: (
|
||||
messageFormat: SlackMessageBodyFormat | undefined,
|
||||
) => Promise<SlackToolResult>;
|
||||
};
|
||||
|
||||
export const sendSlackMessageWithMarkdownFallback = async ({
|
||||
messageFormat,
|
||||
failureMessage,
|
||||
sendMessage,
|
||||
}: SendSlackMessageWithMarkdownFallbackParams): Promise<SlackToolResult> => {
|
||||
try {
|
||||
return await sendMessage(messageFormat);
|
||||
} catch (error) {
|
||||
if (messageFormat !== 'markdown' || !isSlackMarkdownFormatError(error)) {
|
||||
return slackToolFailure(failureMessage, error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return await sendMessage('plain');
|
||||
} catch (error) {
|
||||
return slackToolFailure(failureMessage, error);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user