@trigger.dev/slack: Added joinConversation task, and automatically try and join a public conversation if the bot is not in it when posting a message

This commit is contained in:
Eric Allam
2023-07-03 17:48:10 +01:00
parent 6c9ca686cb
commit f210532870
10 changed files with 179 additions and 24 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@trigger.dev/slack": patch
---
Added joinConversation task, and automatically try and join a public conversation if the bot is not in it when posting a message
@@ -28,10 +28,10 @@ export class IntegrationConnectionCreatedService {
const missingConnection = await tx.missingConnection.findUnique({ const missingConnection = await tx.missingConnection.findUnique({
where: { where: {
integrationId_connectionType_externalAccountId: { integrationId_connectionType_accountIdentifier: {
integrationId: connection.integrationId, integrationId: connection.integrationId,
connectionType: connection.connectionType, connectionType: connection.connectionType,
externalAccountId: connection.externalAccount accountIdentifier: connection.externalAccount
? connection.externalAccount.id ? connection.externalAccount.id
: "DEVELOPER", : "DEVELOPER",
}, },
@@ -49,7 +49,7 @@ export const slack: Integration = {
authenticationMethods: { authenticationMethods: {
oauth2Bot: { oauth2Bot: {
name: "OAuth2 (Bot)", name: "OAuth2 (Bot)",
description: "Authenticate as a bot", description: "Authenticate as a bot. This is the recommended method.",
type: "oauth2", type: "oauth2",
client: { client: {
id: { id: {
@@ -111,6 +111,7 @@ export const slack: Integration = {
{ {
name: "channels:join", name: "channels:join",
description: "Join public channels in a workspace", description: "Join public channels in a workspace",
defaultChecked: true,
}, },
{ {
name: "channels:manage", name: "channels:manage",
@@ -141,17 +142,20 @@ export const slack: Integration = {
{ {
name: "chat:write", name: "chat:write",
description: "Post messages in approved channels & conversations", description: "Post messages in approved channels & conversations",
defaultChecked: true,
}, },
{ {
name: "chat:write.customize", name: "chat:write.customize",
description: description:
"Send messages as @your_slack_app with a customized username and avatar", "Send messages as @your_slack_app with a customized username and avatar",
defaultChecked: true,
}, },
{ {
name: "chat:write.public", name: "chat:write.public",
description: description:
"Send messages to channels @your_slack_app isn't a member of", "Send messages to channels @your_slack_app isn't a member of",
defaultChecked: true,
}, },
{ {
@@ -591,10 +595,12 @@ export const slack: Integration = {
{ {
name: "chat:write:bot", name: "chat:write:bot",
description: "Send messages as your slack app", description: "Send messages as your slack app",
defaultChecked: true,
}, },
{ {
name: "chat:write:user", name: "chat:write:user",
description: "Send messages on a users behalf", description: "Send messages on a users behalf",
defaultChecked: true,
}, },
{ {
@@ -195,16 +195,17 @@ export class StartRunService {
missingConnections: { missingConnections: {
connectOrCreate: missingConnections.map((connection) => ({ connectOrCreate: missingConnections.map((connection) => ({
where: { where: {
integrationId_connectionType_externalAccountId: { integrationId_connectionType_accountIdentifier: {
integrationId: connection.integration.id, integrationId: connection.integration.id,
connectionType: connection.connectionType, connectionType: connection.connectionType,
externalAccountId: connection.externalAccountId ?? "DEVELOPER", accountIdentifier: connection.externalAccountId ?? "DEVELOPER",
}, },
}, },
create: { create: {
integrationId: connection.integration.id, integrationId: connection.integration.id,
connectionType: connection.connectionType, connectionType: connection.connectionType,
externalAccountId: connection.externalAccountId ?? "DEVELOPER", accountIdentifier: connection.externalAccountId ?? "DEVELOPER",
externalAccountId: connection.externalAccountId,
resolved: false, resolved: false,
}, },
})), })),
+38 -2
View File
@@ -1,6 +1,6 @@
import { client } from "@/trigger"; import { client } from "@/trigger";
import { Slack } from "@trigger.dev/slack"; import { Slack } from "@trigger.dev/slack";
import { Job, cronTrigger } from "@trigger.dev/sdk"; import { Job, cronTrigger, eventTrigger } from "@trigger.dev/sdk";
const db = { const db = {
getKpiSummary: async (date: Date) => { getKpiSummary: async (date: Date) => {
@@ -11,7 +11,7 @@ const db = {
}, },
}; };
export const slack = new Slack({ id: "slack" }); export const slack = new Slack({ id: "slack-6" });
new Job(client, { new Job(client, {
id: "slack-kpi-summary", id: "slack-kpi-summary",
@@ -33,3 +33,39 @@ new Job(client, {
return response; return response;
}, },
}); });
new Job(client, {
id: "slack-auto-join",
name: "Slack Auto Join",
version: "0.1.1",
integrations: {
slack,
},
trigger: eventTrigger({
name: "slack.auto_join",
}),
run: async (payload, io, ctx) => {
const response = await io.slack.postMessage("Slack 📝", {
channel: "C05G130TH4G",
text: "Welcome to the team, Eric!",
blocks: [
{
type: "section",
text: {
type: "mrkdwn",
text: `Welcome to the team, Eric!`,
},
},
{
type: "section",
text: {
type: "mrkdwn",
text: `I'm here to help you get started with Trigger!`,
},
},
],
});
return response;
},
});
@@ -1,8 +1,8 @@
import { client } from "@/trigger"; import { client } from "@/trigger";
import "@/jobs/github"; // import "@/jobs/github";
import "@/jobs/openai"; // import "@/jobs/openai";
import "@/jobs/resend"; // import "@/jobs/resend";
import "@/jobs/general"; // import "@/jobs/general";
import "@/jobs/slack"; import "@/jobs/slack";
import "@/jobs/logging"; import "@/jobs/logging";
import { createPagesRoute } from "@trigger.dev/nextjs"; import { createPagesRoute } from "@trigger.dev/nextjs";
+2 -1
View File
@@ -1,10 +1,11 @@
import { WebClient } from "@slack/web-api"; import { WebClient } from "@slack/web-api";
import type { IntegrationClient, TriggerIntegration } from "@trigger.dev/sdk"; import type { IntegrationClient, TriggerIntegration } from "@trigger.dev/sdk";
import { clientFactory } from "./client"; import { clientFactory } from "./client";
import { postMessage } from "./tasks"; import { joinConversation, postMessage } from "./tasks";
const tasks = { const tasks = {
postMessage, postMessage,
joinConversation,
}; };
export type SlackIntegrationOptions = { export type SlackIntegrationOptions = {
+101 -9
View File
@@ -1,19 +1,85 @@
import type {
Block,
KnownBlock,
MessageAttachment,
MessageMetadata,
WebAPIPlatformError,
} from "@slack/web-api";
import { clientFactory } from "./client"; import { clientFactory } from "./client";
import type { AuthenticatedTask } from "@trigger.dev/sdk"; import type { AuthenticatedTask } from "@trigger.dev/sdk";
type SlackClientType = ReturnType<typeof clientFactory>; type SlackClientType = ReturnType<typeof clientFactory>;
export type ChatPostMessageArguments = {
channel: string;
text?: string;
as_user?: boolean;
attachments?: MessageAttachment[];
blocks?: (KnownBlock | Block)[];
icon_emoji?: string;
icon_url?: string;
metadata?: MessageMetadata;
link_names?: boolean;
mrkdwn?: boolean;
parse?: "full" | "none";
reply_broadcast?: boolean;
thread_ts?: string;
unfurl_links?: boolean;
unfurl_media?: boolean;
username?: string;
};
function isPlatformError(error: unknown): error is WebAPIPlatformError {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
typeof error.code === "string" &&
error.code === "slack_webapi_platform_error"
);
}
export const postMessage: AuthenticatedTask< export const postMessage: AuthenticatedTask<
ReturnType<typeof clientFactory>, ReturnType<typeof clientFactory>,
{ text: string; channel: string }, ChatPostMessageArguments,
Awaited<ReturnType<SlackClientType["chat"]["postMessage"]>> Awaited<ReturnType<SlackClientType["chat"]["postMessage"]>>
> = { > = {
run: async (params, client) => { run: async (params, client, task, io, auth) => {
return client.chat.postMessage({ try {
text: params.text, const response = await client.chat.postMessage(params);
channel: params.channel,
link_names: true, return response;
}); } catch (error) {
if (isPlatformError(error)) {
if (error.data.error === "not_in_channel") {
// @ts-ignore
const joinResponse = await io.runTask<ConversationsJoinResponse>(
`Join ${params.channel}`,
joinConversation.init(params),
// @ts-ignore
async (t, io) => {
const subResponse = await joinConversation.run(
{ channel: params.channel },
client,
t,
io,
auth
);
return subResponse;
}
);
if (joinResponse.ok) {
const response = await client.chat.postMessage(params);
return response;
}
}
}
throw error;
}
}, },
init: (params) => { init: (params) => {
return { return {
@@ -25,9 +91,35 @@ export const postMessage: AuthenticatedTask<
label: "Channel ID", label: "Channel ID",
text: params.channel, text: params.channel,
}, },
...(params.text ? [{ label: "Message", text: params.text }] : []),
],
};
},
};
type ConversationsJoinResponse = Awaited<
ReturnType<SlackClientType["conversations"]["join"]>
>;
export const joinConversation: AuthenticatedTask<
ReturnType<typeof clientFactory>,
{ channel: string },
ConversationsJoinResponse
> = {
run: async (params, client, task, io, auth) => {
const response = await client.conversations.join(params);
return response;
},
init: (params) => {
return {
name: "Join Channel",
params,
icon: "slack",
properties: [
{ {
label: "Message", label: "Channel ID",
text: params.text, text: params.channel,
}, },
], ],
}; };
@@ -0,0 +1,11 @@
/*
Warnings:
- A unique constraint covering the columns `[integrationId,connectionType,accountIdentifier]` on the table `MissingConnection` will be added. If there are existing duplicate values, this will fail.
*/
-- AlterTable
ALTER TABLE "MissingConnection" ADD COLUMN "accountIdentifier" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "MissingConnection_integrationId_connectionType_accountIdent_key" ON "MissingConnection"("integrationId", "connectionType", "accountIdentifier");
+5 -2
View File
@@ -122,7 +122,7 @@ model IntegrationDefinition {
name String name String
instructions String? instructions String?
description String? description String?
packageName String @default("") packageName String @default("")
authMethods IntegrationAuthMethod[] authMethods IntegrationAuthMethod[]
Integration Integration[] Integration Integration[]
@@ -137,7 +137,7 @@ model Integration {
description String? description String?
setupStatus IntegrationSetupStatus @default(COMPLETE) setupStatus IntegrationSetupStatus @default(COMPLETE)
authSource IntegrationAuthSource @default(HOSTED) authSource IntegrationAuthSource @default(HOSTED)
definition IntegrationDefinition @relation(fields: [definitionId], references: [id], onDelete: Cascade, onUpdate: Cascade) definition IntegrationDefinition @relation(fields: [definitionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
definitionId String definitionId String
@@ -987,10 +987,13 @@ model MissingConnection {
externalAccount ExternalAccount? @relation(fields: [externalAccountId], references: [id], onDelete: Cascade, onUpdate: Cascade) externalAccount ExternalAccount? @relation(fields: [externalAccountId], references: [id], onDelete: Cascade, onUpdate: Cascade)
externalAccountId String? externalAccountId String?
accountIdentifier String?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@unique([integrationId, connectionType, externalAccountId]) @@unique([integrationId, connectionType, externalAccountId])
@@unique([integrationId, connectionType, accountIdentifier])
} }
model ApiIntegrationVote { model ApiIntegrationVote {