@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({
where: {
integrationId_connectionType_externalAccountId: {
integrationId_connectionType_accountIdentifier: {
integrationId: connection.integrationId,
connectionType: connection.connectionType,
externalAccountId: connection.externalAccount
accountIdentifier: connection.externalAccount
? connection.externalAccount.id
: "DEVELOPER",
},
@@ -49,7 +49,7 @@ export const slack: Integration = {
authenticationMethods: {
oauth2Bot: {
name: "OAuth2 (Bot)",
description: "Authenticate as a bot",
description: "Authenticate as a bot. This is the recommended method.",
type: "oauth2",
client: {
id: {
@@ -111,6 +111,7 @@ export const slack: Integration = {
{
name: "channels:join",
description: "Join public channels in a workspace",
defaultChecked: true,
},
{
name: "channels:manage",
@@ -141,17 +142,20 @@ export const slack: Integration = {
{
name: "chat:write",
description: "Post messages in approved channels & conversations",
defaultChecked: true,
},
{
name: "chat:write.customize",
description:
"Send messages as @your_slack_app with a customized username and avatar",
defaultChecked: true,
},
{
name: "chat:write.public",
description:
"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",
description: "Send messages as your slack app",
defaultChecked: true,
},
{
name: "chat:write:user",
description: "Send messages on a users behalf",
defaultChecked: true,
},
{
@@ -195,16 +195,17 @@ export class StartRunService {
missingConnections: {
connectOrCreate: missingConnections.map((connection) => ({
where: {
integrationId_connectionType_externalAccountId: {
integrationId_connectionType_accountIdentifier: {
integrationId: connection.integration.id,
connectionType: connection.connectionType,
externalAccountId: connection.externalAccountId ?? "DEVELOPER",
accountIdentifier: connection.externalAccountId ?? "DEVELOPER",
},
},
create: {
integrationId: connection.integration.id,
connectionType: connection.connectionType,
externalAccountId: connection.externalAccountId ?? "DEVELOPER",
accountIdentifier: connection.externalAccountId ?? "DEVELOPER",
externalAccountId: connection.externalAccountId,
resolved: false,
},
})),
+38 -2
View File
@@ -1,6 +1,6 @@
import { client } from "@/trigger";
import { Slack } from "@trigger.dev/slack";
import { Job, cronTrigger } from "@trigger.dev/sdk";
import { Job, cronTrigger, eventTrigger } from "@trigger.dev/sdk";
const db = {
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, {
id: "slack-kpi-summary",
@@ -33,3 +33,39 @@ new Job(client, {
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 "@/jobs/github";
import "@/jobs/openai";
import "@/jobs/resend";
import "@/jobs/general";
// import "@/jobs/github";
// import "@/jobs/openai";
// import "@/jobs/resend";
// import "@/jobs/general";
import "@/jobs/slack";
import "@/jobs/logging";
import { createPagesRoute } from "@trigger.dev/nextjs";
+2 -1
View File
@@ -1,10 +1,11 @@
import { WebClient } from "@slack/web-api";
import type { IntegrationClient, TriggerIntegration } from "@trigger.dev/sdk";
import { clientFactory } from "./client";
import { postMessage } from "./tasks";
import { joinConversation, postMessage } from "./tasks";
const tasks = {
postMessage,
joinConversation,
};
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 type { AuthenticatedTask } from "@trigger.dev/sdk";
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<
ReturnType<typeof clientFactory>,
{ text: string; channel: string },
ChatPostMessageArguments,
Awaited<ReturnType<SlackClientType["chat"]["postMessage"]>>
> = {
run: async (params, client) => {
return client.chat.postMessage({
text: params.text,
channel: params.channel,
link_names: true,
});
run: async (params, client, task, io, auth) => {
try {
const response = await client.chat.postMessage(params);
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) => {
return {
@@ -25,9 +91,35 @@ export const postMessage: AuthenticatedTask<
label: "Channel ID",
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",
text: params.text,
label: "Channel ID",
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
instructions String?
description String?
packageName String @default("")
packageName String @default("")
authMethods IntegrationAuthMethod[]
Integration Integration[]
@@ -137,7 +137,7 @@ model Integration {
description String?
setupStatus IntegrationSetupStatus @default(COMPLETE)
authSource IntegrationAuthSource @default(HOSTED)
authSource IntegrationAuthSource @default(HOSTED)
definition IntegrationDefinition @relation(fields: [definitionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
definitionId String
@@ -987,10 +987,13 @@ model MissingConnection {
externalAccount ExternalAccount? @relation(fields: [externalAccountId], references: [id], onDelete: Cascade, onUpdate: Cascade)
externalAccountId String?
accountIdentifier String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([integrationId, connectionType, externalAccountId])
@@unique([integrationId, connectionType, accountIdentifier])
}
model ApiIntegrationVote {