From 12dfc8dbe1f823372e19397595d0b8cbd833b671 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Tue, 24 Jan 2023 16:40:10 -0800
Subject: [PATCH 01/39] =?UTF-8?q?Added=20scope=20that=20should=20allow=20p?=
=?UTF-8?q?osting=20as=20a=20custom=20name=20=E2=80=9Cchat:write.customize?=
=?UTF-8?q?"?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/trigger-providers/src/providers/slack/index.ts | 1 +
1 file changed, 1 insertion(+)
diff --git a/packages/trigger-providers/src/providers/slack/index.ts b/packages/trigger-providers/src/providers/slack/index.ts
index 03461c695a3..e3149c0582c 100644
--- a/packages/trigger-providers/src/providers/slack/index.ts
+++ b/packages/trigger-providers/src/providers/slack/index.ts
@@ -15,6 +15,7 @@ export const slack = {
"groups:write",
"im:write",
"mpim:write",
+ "chat:write.customize",
],
},
schemas,
From 809ffe2168e660b6c8c36b4882b0952e4ffea183 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Tue, 24 Jan 2023 16:40:34 -0800
Subject: [PATCH 02/39] Added Slack blocks schemas
---
.../src/providers/slack/blocks.ts | 775 ++++++++++++++++++
.../src/providers/slack/schemas.ts | 9 +
2 files changed, 784 insertions(+)
create mode 100644 packages/trigger-providers/src/providers/slack/blocks.ts
diff --git a/packages/trigger-providers/src/providers/slack/blocks.ts b/packages/trigger-providers/src/providers/slack/blocks.ts
new file mode 100644
index 00000000000..a2ed9fed64e
--- /dev/null
+++ b/packages/trigger-providers/src/providers/slack/blocks.ts
@@ -0,0 +1,775 @@
+import { z } from "zod";
+
+export const imageElementSchema = z.object({
+ type: z.literal("image"),
+ image_url: z.string(),
+ alt_text: z.string(),
+});
+
+export const plainTextElementSchema = z.object({
+ type: z.literal("plain_text"),
+ text: z.string(),
+ emoji: z.boolean().optional(),
+});
+
+export const mrkdwnElementSchema = z.object({
+ type: z.literal("mrkdwn"),
+ text: z.string(),
+ verbatim: z.boolean().optional(),
+});
+
+export const mrkdwnOptionSchema = z.object({
+ text: mrkdwnElementSchema,
+ value: z.string().optional(),
+ url: z.string().optional(),
+ description: plainTextElementSchema.optional(),
+});
+
+export const plainTextOptionSchema = z.object({
+ text: plainTextElementSchema,
+ value: z.string().optional(),
+ url: z.string().optional(),
+ description: plainTextElementSchema.optional(),
+});
+
+export const optionSchema = z.union([
+ mrkdwnOptionSchema,
+ plainTextOptionSchema,
+]);
+
+export const confirmSchema = z.object({
+ title: plainTextElementSchema.optional(),
+ text: z.union([plainTextElementSchema, mrkdwnElementSchema]),
+ confirm: plainTextElementSchema.optional(),
+ deny: plainTextElementSchema.optional(),
+ style: z.union([z.literal("primary"), z.literal("danger")]).optional(),
+});
+
+/**
+ * @description Determines when an input element will return a
+ * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` interaction payload}.
+ */
+export const dispatchActionConfigSchema = z.object({
+ /**
+ * @description An array of interaction types that you would like to receive a
+ * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` payload} for. Should be
+ * one or both of:
+ * `on_enter_pressed` — payload is dispatched when user presses the enter key while the input is in focus. Hint
+ * text will appear underneath the input explaining to the user to press enter to submit.
+ * `on_character_entered` — payload is dispatched when a character is entered (or removed) in the input.
+ */
+ trigger_actions_on: z
+ .array(
+ z.union([
+ z.literal("on_enter_pressed"),
+ z.literal("on_character_entered"),
+ ])
+ )
+ .optional(),
+});
+
+export const actionSchema = z.object({
+ type: z.string(),
+ /**
+ * @description: An identifier for this action. You can use this when you receive an interaction payload to
+ * {@link https://api.slack.com/interactivity/handling#payloads identify the source of the action}. Should be unique
+ * among all other `action_id`s in the containing block. Maximum length for this field is 255 characters.
+ */
+ action_id: z.string().optional(),
+});
+
+export const confirmableSchema = z.object({
+ /**
+ * @description A {@see Confirm} object that defines an optional confirmation dialog after the element is interacted
+ * with.
+ */
+ confirm: confirmSchema.optional(),
+});
+
+export const focusableSchema = z.object({
+ /**
+ * @description Indicates whether the element will be set to auto focus within the
+ * {@link https://api.slack.com/reference/surfaces/views `view` object}. Only one element can be set to `true`.
+ * Defaults to `false`.
+ */
+ focus_on_load: z.boolean().optional(),
+});
+
+export const placeholdableSchema = z.object({
+ /**
+ * @description A {@see PlainTextElement} object that defines the placeholder text shown on the element. Maximum
+ * length for the `text` field in this object is 150 characters.
+ */
+ placeholder: plainTextElementSchema.optional(),
+});
+
+export const dispatchableSchema = z.object({
+ /**
+ * @description A {@see DispatchActionConfig} object that determines when during text input the element returns a
+ * {@link https://api.slack.com/reference/interaction-payloads/block-actions `block_actions` payload}.
+ */
+ dispatch_action_config: dispatchActionConfigSchema.optional(),
+});
+
+export const usersSelectSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("users_select"),
+ initial_user: z.string().optional(),
+ });
+
+export const multiUsersSelectSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("multi_users_select"),
+ initial_users: z.array(z.string()).optional(),
+ max_selected_items: z.number().optional(),
+ });
+
+export const staticSelectSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("static_select"),
+ initial_option: plainTextOptionSchema.optional(),
+ options: z.array(plainTextOptionSchema).optional(),
+ option_groups: z
+ .array(
+ z.object({
+ label: plainTextElementSchema,
+ options: z.array(plainTextOptionSchema),
+ })
+ )
+ .optional(),
+ });
+
+export const multiStaticSelectSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("multi_static_select"),
+ initial_options: z.array(plainTextOptionSchema).optional(),
+ options: z.array(plainTextOptionSchema).optional(),
+ option_groups: z
+ .array(
+ z.object({
+ label: plainTextElementSchema,
+ options: z.array(plainTextOptionSchema),
+ })
+ )
+ .optional(),
+ max_selected_items: z.number().optional(),
+ });
+
+export const conversationsSelectSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("conversations_select"),
+ initial_conversation: z.string().optional(),
+ response_url_enabled: z.boolean().optional(),
+ default_to_current_conversation: z.boolean().optional(),
+ filter: z
+ .object({
+ include: z
+ .array(
+ z.union([
+ z.literal("im"),
+ z.literal("mpim"),
+ z.literal("private"),
+ z.literal("public"),
+ ])
+ )
+ .optional(),
+ exclude_external_shared_channels: z.boolean().optional(),
+ exclude_bot_users: z.boolean().optional(),
+ })
+ .optional(),
+ });
+
+export const multiConversationsSelectSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("multi_conversations_select"),
+ initial_conversations: z.array(z.string()).optional(),
+ max_selected_items: z.number().optional(),
+ default_to_current_conversation: z.boolean().optional(),
+ filter: z
+ .object({
+ include: z
+ .array(
+ z.union([
+ z.literal("im"),
+ z.literal("mpim"),
+ z.literal("private"),
+ z.literal("public"),
+ ])
+ )
+ .optional(),
+ exclude_external_shared_channels: z.boolean().optional(),
+ exclude_bot_users: z.boolean().optional(),
+ })
+ .optional(),
+ });
+
+export const channelsSelectSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("channels_select"),
+ initial_channel: z.string().optional(),
+ });
+
+export const multiChannelsSelectSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("multi_channels_select"),
+ initial_channels: z.array(z.string()).optional(),
+ max_selected_items: z.number().optional(),
+ });
+
+export const externalSelectSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("external_select"),
+ initial_option: plainTextOptionSchema.optional(),
+ min_query_length: z.number().optional(),
+ });
+
+export const multiExternalSelectSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("multi_external_select"),
+ initial_options: z.array(plainTextOptionSchema).optional(),
+ min_query_length: z.number().optional(),
+ max_selected_items: z.number().optional(),
+ });
+
+export const buttonSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend({
+ type: z.literal("button"),
+ text: plainTextElementSchema,
+ value: z.string().optional(),
+ url: z.string().optional(),
+ style: z.union([z.literal("danger"), z.literal("primary")]).optional(),
+ accessibility_label: z.string().optional(),
+ });
+
+export const overflowSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend({
+ type: z.literal("overflow"),
+ options: z.array(plainTextOptionSchema),
+ });
+
+export const datepickerSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("datepicker"),
+ initial_date: z.string().optional(),
+ });
+
+export const timepickerSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("timepicker"),
+ initial_time: z.string().optional(),
+ timezone: z.string().optional(),
+ });
+
+export const radioButtonsSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend({
+ type: z.literal("radio_buttons"),
+ initial_option: optionSchema.optional(),
+ options: z.array(optionSchema),
+ });
+
+/**
+ * @description An element that allows the selection of a time of day formatted as a UNIX timestamp. On desktop
+ * clients, this time picker will take the form of a dropdown list and the date picker will take the form of a dropdown
+ * calendar. Both options will have free-text entry for precise choices. On mobile clients, the time picker and date
+ * picker will use native UIs.
+ * {@link https://api.slack.com/reference/block-kit/block-elements#datetimepicker}
+ */
+export const dateTimepickerSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend({
+ type: z.literal("datetimepicker"),
+ /**
+ * @description The initial date and time that is selected when the element is loaded, represented as a UNIX
+ * timestamp in seconds. This should be in the format of 10 digits, for example 1628633820 represents the date and
+ * time August 10th, 2021 at 03:17pm PST.
+ */
+ initial_date_time: z.number().optional(),
+ });
+
+export const checkboxesSchema = actionSchema
+ .extend(confirmableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend({
+ type: z.literal("checkboxes"),
+ initial_options: z.array(optionSchema).optional(),
+ options: z.array(optionSchema),
+ });
+
+export const plainTextInputSchema = actionSchema
+ .extend(dispatchableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("plain_text_input"),
+ initial_value: z.string().optional(),
+ multiline: z.boolean().optional(),
+ min_length: z.number().optional(),
+ max_length: z.number().optional(),
+ dispatch_action_config: dispatchActionConfigSchema.optional(),
+ focus_on_load: z.boolean().optional(),
+ });
+
+/**
+ * @description A URL input element, similar to the {@see PlainTextInput} element, creates a single line field where
+ * a user can enter URL-encoded data.
+ * {@link https://api.slack.com/reference/block-kit/block-elements#url}
+ */
+export const uRLInputSchema = actionSchema
+ .extend(dispatchableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("url_text_input"),
+ /**
+ * @description The initial value in the URL input when it is loaded.
+ */
+ initial_value: z.string().optional(),
+ });
+
+/**
+ * @description An email input element, similar to the {@see PlainTextInput} element, creates a single line field where
+ * a user can enter an email address.
+ * {@link https://api.slack.com/reference/block-kit/block-elements#email}
+ */
+export const emailInputSchema = actionSchema
+ .extend(dispatchableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("email_text_input"),
+ /**
+ * @description The initial value in the email input when it is loaded.
+ */
+ initial_value: z.string().optional(),
+ });
+
+/**
+ * @description A number input element, similar to the {@see PlainTextInput} element, creates a single line field where
+ * a user can a number. This input elements accepts floating point numbers, for example, 0.25, 5.5, and -10 are all
+ * valid input values. Decimal numbers are only allowed when `is_decimal_allowed` is equal to `true`.
+ * {@link https://api.slack.com/reference/block-kit/block-elements#number}
+ */
+export const numberInputSchema = actionSchema
+ .extend(dispatchableSchema.shape)
+ .extend(focusableSchema.shape)
+ .extend(placeholdableSchema.shape)
+ .extend({
+ type: z.literal("number_input"),
+ /**
+ * @description Decimal numbers are allowed if this property is `true`, set the value to `false` otherwise.
+ */
+ is_decimal_allowed: z.boolean(),
+ /**
+ * @description The initial value in the input when it is loaded.
+ */
+ initial_value: z.string().optional(),
+ /**
+ * @description The minimum value, cannot be greater than `max_value`.
+ */
+ min_value: z.string().optional(),
+ /**
+ * @description The maximum value, cannot be less than `min_value`.
+ */
+ max_value: z.string().optional(),
+ });
+
+export const blockSchema = z.object({
+ type: z.string(),
+ block_id: z.string().optional(),
+});
+
+export const imageBlockSchema = blockSchema.extend({
+ type: z.literal("image"),
+ image_url: z.string(),
+ alt_text: z.string(),
+ title: plainTextElementSchema.optional(),
+});
+
+export const contextBlockSchema = blockSchema.extend({
+ type: z.literal("context"),
+ elements: z.array(
+ z.union([imageElementSchema, plainTextElementSchema, mrkdwnElementSchema])
+ ),
+});
+
+export const dividerBlockSchema = blockSchema.extend({
+ type: z.literal("divider"),
+});
+
+export const fileBlockSchema = blockSchema.extend({
+ type: z.literal("file"),
+ source: z.string(),
+ external_id: z.string(),
+});
+
+export const headerBlockSchema = blockSchema.extend({
+ type: z.literal("header"),
+ text: plainTextElementSchema,
+});
+
+export const messageMetadataEventPayloadObjectSchema = z.record(
+ z.union([z.string(), z.number(), z.boolean()])
+);
+
+export const messageAttachmentPreviewSchema = z.object({
+ type: z.string().optional(),
+ can_remove: z.boolean().optional(),
+ title: plainTextElementSchema.optional(),
+ subtitle: plainTextElementSchema.optional(),
+ iconUrl: z.string().optional(),
+});
+
+export const optionFieldSchema = z.object({
+ description: z.string().optional(),
+ text: z.string(),
+ value: z.string(),
+});
+
+export const confirmationSchema = z.object({
+ dismiss_text: z.string().optional(),
+ ok_text: z.string().optional(),
+ text: z.string(),
+ title: z.string().optional(),
+});
+
+export const selectOptionSchema = z.object({
+ label: z.string(),
+ value: z.string(),
+});
+
+export const callUserSlackSchema = z.object({
+ slack_id: z.string(),
+});
+
+export const callUserExternalSchema = z.object({
+ external_id: z.string(),
+ display_name: z.string(),
+ avatar_url: z.string(),
+});
+
+export const videoBlockSchema = blockSchema.extend({
+ type: z.literal("video"),
+ video_url: z.string(),
+ thumbnail_url: z.string(),
+ alt_text: z.string(),
+ title: plainTextElementSchema,
+ title_url: z.string().optional(),
+ author_name: z.string().optional(),
+ provider_name: z.string().optional(),
+ provider_icon_url: z.string().optional(),
+ description: plainTextElementSchema.optional(),
+});
+
+export const dialogSchema = z.object({
+ title: z.string(),
+ callback_id: z.string(),
+ elements: z.array(
+ z.object({
+ type: z.union([
+ z.literal("text"),
+ z.literal("textarea"),
+ z.literal("select"),
+ ]),
+ name: z.string(),
+ label: z.string(),
+ optional: z.boolean().optional(),
+ placeholder: z.string().optional(),
+ value: z.string().optional(),
+ max_length: z.number().optional(),
+ min_length: z.number().optional(),
+ hint: z.string().optional(),
+ subtype: z
+ .union([
+ z.literal("email"),
+ z.literal("number"),
+ z.literal("tel"),
+ z.literal("url"),
+ ])
+ .optional(),
+ data_source: z
+ .union([
+ z.literal("users"),
+ z.literal("channels"),
+ z.literal("conversations"),
+ z.literal("external"),
+ ])
+ .optional(),
+ selected_options: z.array(selectOptionSchema).optional(),
+ options: z.array(selectOptionSchema).optional(),
+ option_groups: z
+ .array(
+ z.object({
+ label: z.string(),
+ options: z.array(selectOptionSchema),
+ })
+ )
+ .optional(),
+ min_query_length: z.number().optional(),
+ })
+ ),
+ submit_label: z.string().optional(),
+ notify_on_cancel: z.boolean().optional(),
+ state: z.string().optional(),
+});
+
+export const selectSchema = z.union([
+ usersSelectSchema,
+ staticSelectSchema,
+ conversationsSelectSchema,
+ channelsSelectSchema,
+ externalSelectSchema,
+]);
+
+export const multiSelectSchema = z.union([
+ multiUsersSelectSchema,
+ multiStaticSelectSchema,
+ multiConversationsSelectSchema,
+ multiChannelsSelectSchema,
+ multiExternalSelectSchema,
+]);
+
+export const actionsBlockSchema = blockSchema.extend({
+ type: z.literal("actions"),
+ elements: z.array(
+ z.union([
+ buttonSchema,
+ overflowSchema,
+ datepickerSchema,
+ timepickerSchema,
+ dateTimepickerSchema,
+ selectSchema,
+ radioButtonsSchema,
+ checkboxesSchema,
+ actionSchema,
+ ])
+ ),
+});
+
+export const sectionBlockSchema = blockSchema.extend({
+ type: z.literal("section"),
+ text: z.union([plainTextElementSchema, mrkdwnElementSchema]).optional(),
+ fields: z
+ .array(z.union([plainTextElementSchema, mrkdwnElementSchema]))
+ .optional(),
+ accessory: z
+ .union([
+ buttonSchema,
+ overflowSchema,
+ datepickerSchema,
+ timepickerSchema,
+ selectSchema,
+ multiSelectSchema,
+ actionSchema,
+ imageElementSchema,
+ radioButtonsSchema,
+ checkboxesSchema,
+ ])
+ .optional(),
+});
+
+export const inputBlockSchema = blockSchema.extend({
+ type: z.literal("input"),
+ label: plainTextElementSchema,
+ hint: plainTextElementSchema.optional(),
+ optional: z.boolean().optional(),
+ element: z.union([
+ selectSchema,
+ multiSelectSchema,
+ datepickerSchema,
+ timepickerSchema,
+ dateTimepickerSchema,
+ plainTextInputSchema,
+ uRLInputSchema,
+ emailInputSchema,
+ numberInputSchema,
+ radioButtonsSchema,
+ checkboxesSchema,
+ ]),
+ dispatch_action: z.boolean().optional(),
+});
+
+export const messageMetadataSchema = z.object({
+ event_type: z.string(),
+ event_payload: z.record(
+ z.union([
+ z.string(),
+ z.number(),
+ z.boolean(),
+ messageMetadataEventPayloadObjectSchema,
+ z.array(messageMetadataEventPayloadObjectSchema),
+ ])
+ ),
+});
+
+export const attachmentActionSchema = z.object({
+ id: z.string().optional(),
+ confirm: confirmationSchema.optional(),
+ data_source: z
+ .union([
+ z.literal("static"),
+ z.literal("channels"),
+ z.literal("conversations"),
+ z.literal("users"),
+ z.literal("external"),
+ ])
+ .optional(),
+ min_query_length: z.number().optional(),
+ name: z.string().optional(),
+ options: z.array(optionFieldSchema).optional(),
+ option_groups: z
+ .array(
+ z.object({
+ text: z.string(),
+ options: z.array(optionFieldSchema),
+ })
+ )
+ .optional(),
+ selected_options: z.array(optionFieldSchema).optional(),
+ style: z
+ .union([z.literal("default"), z.literal("primary"), z.literal("danger")])
+ .optional(),
+ text: z.string(),
+ type: z.union([z.literal("button"), z.literal("select")]),
+ value: z.string().optional(),
+ url: z.string().optional(),
+});
+
+export const callUserSchema = z.union([
+ callUserSlackSchema,
+ callUserExternalSchema,
+]);
+
+export const knownBlockSchema = z.union([
+ imageBlockSchema,
+ contextBlockSchema,
+ actionsBlockSchema,
+ dividerBlockSchema,
+ sectionBlockSchema,
+ inputBlockSchema,
+ fileBlockSchema,
+ headerBlockSchema,
+ videoBlockSchema,
+]);
+
+export const messageAttachmentSchema = z.object({
+ blocks: z.array(z.union([knownBlockSchema, blockSchema])).optional(),
+ fallback: z.string().optional(),
+ color: z
+ .union([
+ z.literal("good"),
+ z.literal("warning"),
+ z.literal("danger"),
+ z.string(),
+ ])
+ .optional(),
+ pretext: z.string().optional(),
+ author_name: z.string().optional(),
+ author_link: z.string().optional(),
+ author_icon: z.string().optional(),
+ title: z.string().optional(),
+ title_link: z.string().optional(),
+ text: z.string().optional(),
+ fields: z
+ .array(
+ z.object({
+ title: z.string(),
+ value: z.string(),
+ short: z.boolean().optional(),
+ })
+ )
+ .optional(),
+ image_url: z.string().optional(),
+ thumb_url: z.string().optional(),
+ footer: z.string().optional(),
+ footer_icon: z.string().optional(),
+ ts: z.string().optional(),
+ actions: z.array(attachmentActionSchema).optional(),
+ callback_id: z.string().optional(),
+ mrkdwn_in: z
+ .array(
+ z.union([z.literal("pretext"), z.literal("text"), z.literal("fields")])
+ )
+ .optional(),
+ app_unfurl_url: z.string().optional(),
+ is_app_unfurl: z.boolean().optional(),
+ app_id: z.string().optional(),
+ bot_id: z.string().optional(),
+ preview: messageAttachmentPreviewSchema.optional(),
+});
+
+export const linkUnfurlsSchema = z.record(messageAttachmentSchema);
+
+export const homeViewSchema = z.object({
+ type: z.literal("home"),
+ blocks: z.array(z.union([knownBlockSchema, blockSchema])),
+ private_metadata: z.string().optional(),
+ callback_id: z.string().optional(),
+ external_id: z.string().optional(),
+});
+
+export const modalViewSchema = z.object({
+ type: z.literal("modal"),
+ title: plainTextElementSchema,
+ blocks: z.array(z.union([knownBlockSchema, blockSchema])),
+ close: plainTextElementSchema.optional(),
+ submit: plainTextElementSchema.optional(),
+ private_metadata: z.string().optional(),
+ callback_id: z.string().optional(),
+ clear_on_close: z.boolean().optional(),
+ notify_on_close: z.boolean().optional(),
+ external_id: z.string().optional(),
+});
+
+export const workflowStepViewSchema = z.object({
+ type: z.literal("workflow_step"),
+ blocks: z.array(z.union([knownBlockSchema, blockSchema])),
+ private_metadata: z.string().optional(),
+ callback_id: z.string().optional(),
+ submit_disabled: z.boolean().optional(),
+ external_id: z.string().optional(),
+});
+
+export const viewSchema: z.ZodUnion<
+ [typeof homeViewSchema, typeof modalViewSchema, typeof workflowStepViewSchema]
+> = z.union([homeViewSchema, modalViewSchema, workflowStepViewSchema]);
diff --git a/packages/trigger-providers/src/providers/slack/schemas.ts b/packages/trigger-providers/src/providers/slack/schemas.ts
index 0a2d2427f0c..064cdb8b10d 100644
--- a/packages/trigger-providers/src/providers/slack/schemas.ts
+++ b/packages/trigger-providers/src/providers/slack/schemas.ts
@@ -1,4 +1,5 @@
import { z } from "zod";
+import { knownBlockSchema } from "./blocks";
export const PostMessageSuccessResponseSchema = z.object({
ok: z.literal(true),
@@ -28,6 +29,10 @@ export const PostMessageResponseSchema = z.discriminatedUnion("ok", [
export const PostMessageBodySchema = z.object({
channel: z.string(),
text: z.string(),
+ blocks: z.array(knownBlockSchema).optional(),
+ username: z.string().optional(),
+ iconEmoji: z.string().optional(),
+ iconUrl: z.string().optional(),
});
export const ChannelNameOrIdSchema = z.union([
@@ -38,6 +43,10 @@ export const ChannelNameOrIdSchema = z.union([
export const PostMessageOptionsSchema = z
.object({
text: z.string(),
+ blocks: z.array(knownBlockSchema).optional(),
+ username: z.string().optional(),
+ iconEmoji: z.string().optional(),
+ iconUrl: z.string().optional(),
})
.and(ChannelNameOrIdSchema);
From 1843a2f944ad5f7d91359c88b65ecd0c9847279d Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Tue, 24 Jan 2023 20:12:43 -0800
Subject: [PATCH 03/39] Created a zod helper that can parse from snake_case to
camelCase and vice versa
---
packages/trigger-providers/src/schemaUtils.ts | 86 +++++++++++++++++++
1 file changed, 86 insertions(+)
create mode 100644 packages/trigger-providers/src/schemaUtils.ts
diff --git a/packages/trigger-providers/src/schemaUtils.ts b/packages/trigger-providers/src/schemaUtils.ts
new file mode 100644
index 00000000000..7d3ca62b851
--- /dev/null
+++ b/packages/trigger-providers/src/schemaUtils.ts
@@ -0,0 +1,86 @@
+import { z } from "zod";
+import { CamelCasedPropertiesDeep, SnakeCasedPropertiesDeep } from "type-fest";
+
+/** Converts a Zod Schema from snake_case to camelCase */
+export function snakeToCamel(
+ schema: z.ZodType
+) {
+ return schema.transform(
+ (object) =>
+ deepSnakeToCamel(object) as unknown as CamelCasedPropertiesDeep<
+ typeof object
+ >
+ );
+}
+
+/** Converts a Zod Schema from camelCase to snake_case */
+export function camelToSnake(
+ schema: z.ZodType
+) {
+ return schema.transform(
+ (object) =>
+ deepCamelToSnake(object) as unknown as SnakeCasedPropertiesDeep<
+ typeof object
+ >
+ );
+}
+
+function deepSnakeToCamel(o: any): T {
+ if (isObject(o)) {
+ const n = {};
+
+ Object.keys(o).forEach((k) => {
+ // @ts-ignore
+ n[keySnakeToCamel(k)] = deepSnakeToCamel(o[k]);
+ });
+
+ return n as T;
+ } else if (isArray(o)) {
+ return o.map((i: any) => {
+ return deepSnakeToCamel(i);
+ });
+ }
+
+ return o;
+}
+
+function keySnakeToCamel(s: string): string {
+ return s.replace(/([_][a-z])/gi, ($1) => {
+ return $1.toUpperCase().replace("-", "").replace("_", "");
+ });
+}
+
+function deepCamelToSnake(o: any): T {
+ if (isObject(o)) {
+ const n = {};
+
+ Object.keys(o).forEach((k) => {
+ // @ts-ignore
+ n[keyCamelToSnake(k)] = deepCamelToSnake(o[k]);
+ });
+
+ return n as T;
+ } else if (isArray(o)) {
+ return o.map((i: any) => {
+ return deepCamelToSnake(i);
+ });
+ }
+
+ return o;
+}
+
+function keyCamelToSnake(s: string): string {
+ return s
+ .replace(/[\w]([A-Z])/g, function (m) {
+ return m[0] + "_" + m[1];
+ })
+ .toLowerCase();
+}
+
+function isArray(a: any): boolean {
+ return Array.isArray(a);
+}
+
+function isObject(o: any): boolean {
+ return o === Object(o) && !isArray(o) && typeof o !== "function";
+}
From 51c295f10216244f348c698efc04f0dedf92a936 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Tue, 24 Jan 2023 20:13:04 -0800
Subject: [PATCH 04/39] Added type-fest to trigger-providers as a dev
dependency
---
packages/trigger-providers/package.json | 1 +
pnpm-lock.yaml | 7 +++++++
2 files changed, 8 insertions(+)
diff --git a/packages/trigger-providers/package.json b/packages/trigger-providers/package.json
index 4d427998fd5..5d9f97711da 100644
--- a/packages/trigger-providers/package.json
+++ b/packages/trigger-providers/package.json
@@ -22,6 +22,7 @@
"@types/node": "16",
"rimraf": "^3.0.2",
"tsup": "^6.5.0",
+ "type-fest": "^3.5.3",
"typescript": "^4.9.4"
},
"scripts": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ba2fc009ae1..bab4da4cf12 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -790,6 +790,7 @@ importers:
rimraf: ^3.0.2
tiny-invariant: ^1.2.0
tsup: ^6.5.0
+ type-fest: ^3.5.3
typescript: ^4.9.4
zod: ^3.20.2
dependencies:
@@ -801,6 +802,7 @@ importers:
'@types/node': 16.18.11
rimraf: 3.0.2
tsup: 6.5.0_typescript@4.9.4
+ type-fest: 3.5.3
typescript: 4.9.4
packages/trigger-sdk:
@@ -16018,6 +16020,11 @@ packages:
engines: {node: '>=14.16'}
dev: true
+ /type-fest/3.5.3:
+ resolution: {integrity: sha512-V2+og4j/rWReWvaFrse3s9g2xvUv/K9Azm/xo6CjIuq7oeGqsoimC7+9/A3tfvNcbQf8RPSVj/HV81fB4DJrjA==}
+ engines: {node: '>=14.16'}
+ dev: true
+
/type-is/1.6.18:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'}
From 001cd811c4034a410e0e4d9b57a1c5721e0676c8 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Tue, 24 Jan 2023 20:13:40 -0800
Subject: [PATCH 05/39] Added Slack block schemas
---
.../src/providers/slack/blocks.ts | 80 ++++++++++++++-----
.../src/providers/slack/schemas.ts | 8 +-
2 files changed, 63 insertions(+), 25 deletions(-)
diff --git a/packages/trigger-providers/src/providers/slack/blocks.ts b/packages/trigger-providers/src/providers/slack/blocks.ts
index a2ed9fed64e..ac7ab104b8a 100644
--- a/packages/trigger-providers/src/providers/slack/blocks.ts
+++ b/packages/trigger-providers/src/providers/slack/blocks.ts
@@ -39,7 +39,10 @@ export const optionSchema = z.union([
export const confirmSchema = z.object({
title: plainTextElementSchema.optional(),
- text: z.union([plainTextElementSchema, mrkdwnElementSchema]),
+ text: z.discriminatedUnion("type", [
+ plainTextElementSchema,
+ mrkdwnElementSchema,
+ ]),
confirm: plainTextElementSchema.optional(),
deny: plainTextElementSchema.optional(),
style: z.union([z.literal("primary"), z.literal("danger")]).optional(),
@@ -429,7 +432,11 @@ export const imageBlockSchema = blockSchema.extend({
export const contextBlockSchema = blockSchema.extend({
type: z.literal("context"),
elements: z.array(
- z.union([imageElementSchema, plainTextElementSchema, mrkdwnElementSchema])
+ z.discriminatedUnion("type", [
+ imageElementSchema,
+ plainTextElementSchema,
+ mrkdwnElementSchema,
+ ])
),
});
@@ -553,32 +560,41 @@ export const dialogSchema = z.object({
state: z.string().optional(),
});
-export const selectSchema = z.union([
+const selectSchemas = [
usersSelectSchema,
staticSelectSchema,
conversationsSelectSchema,
channelsSelectSchema,
externalSelectSchema,
+];
+
+export const selectSchema = z.discriminatedUnion("type", [
+ selectSchemas[0],
+ ...selectSchemas.slice(1),
]);
-export const multiSelectSchema = z.union([
+const multiSelectSchemas = [
multiUsersSelectSchema,
multiStaticSelectSchema,
multiConversationsSelectSchema,
multiChannelsSelectSchema,
multiExternalSelectSchema,
+];
+export const multiSelectSchema = z.discriminatedUnion("type", [
+ multiSelectSchemas[0],
+ ...multiSelectSchemas.slice(1),
]);
export const actionsBlockSchema = blockSchema.extend({
type: z.literal("actions"),
elements: z.array(
- z.union([
+ z.discriminatedUnion("type", [
buttonSchema,
overflowSchema,
datepickerSchema,
timepickerSchema,
dateTimepickerSchema,
- selectSchema,
+ ...selectSchemas,
radioButtonsSchema,
checkboxesSchema,
actionSchema,
@@ -588,18 +604,25 @@ export const actionsBlockSchema = blockSchema.extend({
export const sectionBlockSchema = blockSchema.extend({
type: z.literal("section"),
- text: z.union([plainTextElementSchema, mrkdwnElementSchema]).optional(),
+ text: z
+ .discriminatedUnion("type", [plainTextElementSchema, mrkdwnElementSchema])
+ .optional(),
fields: z
- .array(z.union([plainTextElementSchema, mrkdwnElementSchema]))
+ .array(
+ z.discriminatedUnion("type", [
+ plainTextElementSchema,
+ mrkdwnElementSchema,
+ ])
+ )
.optional(),
accessory: z
- .union([
+ .discriminatedUnion("type", [
buttonSchema,
overflowSchema,
datepickerSchema,
timepickerSchema,
- selectSchema,
- multiSelectSchema,
+ ...selectSchemas,
+ ...multiSelectSchemas,
actionSchema,
imageElementSchema,
radioButtonsSchema,
@@ -613,10 +636,10 @@ export const inputBlockSchema = blockSchema.extend({
label: plainTextElementSchema,
hint: plainTextElementSchema.optional(),
optional: z.boolean().optional(),
- element: z.union([
- selectSchema,
- multiSelectSchema,
+ element: z.discriminatedUnion("type", [
datepickerSchema,
+ ...selectSchemas,
+ ...multiSelectSchemas,
timepickerSchema,
dateTimepickerSchema,
plainTextInputSchema,
@@ -680,7 +703,7 @@ export const callUserSchema = z.union([
callUserExternalSchema,
]);
-export const knownBlockSchema = z.union([
+const knownBlocks = [
imageBlockSchema,
contextBlockSchema,
actionsBlockSchema,
@@ -690,10 +713,20 @@ export const knownBlockSchema = z.union([
fileBlockSchema,
headerBlockSchema,
videoBlockSchema,
+];
+
+export const knownBlockSchema = z.discriminatedUnion("type", [
+ knownBlocks[0],
+ ...knownBlocks.slice(1),
+]);
+
+const anyBlockSchema = z.discriminatedUnion("type", [
+ blockSchema,
+ ...knownBlocks,
]);
export const messageAttachmentSchema = z.object({
- blocks: z.array(z.union([knownBlockSchema, blockSchema])).optional(),
+ blocks: z.array(anyBlockSchema).optional(),
fallback: z.string().optional(),
color: z
.union([
@@ -742,7 +775,7 @@ export const linkUnfurlsSchema = z.record(messageAttachmentSchema);
export const homeViewSchema = z.object({
type: z.literal("home"),
- blocks: z.array(z.union([knownBlockSchema, blockSchema])),
+ blocks: z.array(anyBlockSchema),
private_metadata: z.string().optional(),
callback_id: z.string().optional(),
external_id: z.string().optional(),
@@ -751,7 +784,7 @@ export const homeViewSchema = z.object({
export const modalViewSchema = z.object({
type: z.literal("modal"),
title: plainTextElementSchema,
- blocks: z.array(z.union([knownBlockSchema, blockSchema])),
+ blocks: z.array(anyBlockSchema),
close: plainTextElementSchema.optional(),
submit: plainTextElementSchema.optional(),
private_metadata: z.string().optional(),
@@ -763,13 +796,18 @@ export const modalViewSchema = z.object({
export const workflowStepViewSchema = z.object({
type: z.literal("workflow_step"),
- blocks: z.array(z.union([knownBlockSchema, blockSchema])),
+ blocks: z.array(anyBlockSchema),
private_metadata: z.string().optional(),
callback_id: z.string().optional(),
submit_disabled: z.boolean().optional(),
external_id: z.string().optional(),
});
-export const viewSchema: z.ZodUnion<
+export const viewSchema: z.ZodDiscriminatedUnion<
+ "type",
[typeof homeViewSchema, typeof modalViewSchema, typeof workflowStepViewSchema]
-> = z.union([homeViewSchema, modalViewSchema, workflowStepViewSchema]);
+> = z.discriminatedUnion("type", [
+ homeViewSchema,
+ modalViewSchema,
+ workflowStepViewSchema,
+]);
diff --git a/packages/trigger-providers/src/providers/slack/schemas.ts b/packages/trigger-providers/src/providers/slack/schemas.ts
index 064cdb8b10d..dab6b8366fd 100644
--- a/packages/trigger-providers/src/providers/slack/schemas.ts
+++ b/packages/trigger-providers/src/providers/slack/schemas.ts
@@ -31,8 +31,8 @@ export const PostMessageBodySchema = z.object({
text: z.string(),
blocks: z.array(knownBlockSchema).optional(),
username: z.string().optional(),
- iconEmoji: z.string().optional(),
- iconUrl: z.string().optional(),
+ icon_emoji: z.string().optional(),
+ icon_url: z.string().optional(),
});
export const ChannelNameOrIdSchema = z.union([
@@ -45,8 +45,8 @@ export const PostMessageOptionsSchema = z
text: z.string(),
blocks: z.array(knownBlockSchema).optional(),
username: z.string().optional(),
- iconEmoji: z.string().optional(),
- iconUrl: z.string().optional(),
+ icon_emoji: z.string().optional(),
+ icon_url: z.string().optional(),
})
.and(ChannelNameOrIdSchema);
From 2ab9b5f97b4b5812aee9394c08a894363a286700 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Tue, 24 Jan 2023 20:14:02 -0800
Subject: [PATCH 06/39] Build the service request objects ahead of time so we
can easily log them if need to
---
.../src/services/index.ts | 24 ++++++++++---------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/packages/internal-integrations/src/services/index.ts b/packages/internal-integrations/src/services/index.ts
index a1b3baddafc..16f4bd72517 100644
--- a/packages/internal-integrations/src/services/index.ts
+++ b/packages/internal-integrations/src/services/index.ts
@@ -32,17 +32,19 @@ export class HttpService {
endpoint: HttpEndpoint,
body?: z.infer
): Promise> {
- const response = await fetch(
- `${this.options.baseUrl}${endpoint.options.path}`,
- {
- method: endpoint.options.method,
- headers: {
- Authorization: `Bearer ${this.options.accessToken}`,
- "Content-Type": "application/json; charset=utf-8",
- },
- body: body ? JSON.stringify(body) : undefined,
- }
- );
+ const url = `${this.options.baseUrl}${endpoint.options.path}`;
+ const method = endpoint.options.method;
+ const headers = {
+ Authorization: `Bearer ${this.options.accessToken}`,
+ "Content-Type": "application/json; charset=utf-8",
+ };
+ const actualBody = body ? JSON.stringify(body) : undefined;
+
+ const response = await fetch(url, {
+ method,
+ headers,
+ body: actualBody,
+ });
log(
"%s%s response %d",
From 972444da16e045f32734ca9b75c1fd69da96490c Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Tue, 24 Jan 2023 20:14:13 -0800
Subject: [PATCH 07/39] Slack example with lots of blocks
---
examples/send-to-slack/src/index.ts | 89 ++++++++++++++++++++++++++++-
1 file changed, 88 insertions(+), 1 deletion(-)
diff --git a/examples/send-to-slack/src/index.ts b/examples/send-to-slack/src/index.ts
index 62ab9472a59..f5186adf0b2 100644
--- a/examples/send-to-slack/src/index.ts
+++ b/examples/send-to-slack/src/index.ts
@@ -24,6 +24,94 @@ new Trigger({
const response = await slack.postMessage("send-to-slack", {
channelName: "test-integrations",
text: `New domain created: ${event.domain} by customer ${event.customerId} cc @Eric #general`,
+ blocks: [
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: "Hello, Assistant to the Regional Manager Dwight! *Michael Scott* wants to know where you'd like to take the Paper Company investors to dinner tonight.\n\n *Please select a restaurant:*",
+ },
+ },
+ {
+ type: "divider",
+ },
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: "*Farmhouse Thai Cuisine*\n:star::star::star::star: 1528 reviews\n They do have some vegan options, like the roti and curry, plus they have a ton of salad stuff and noodles can be ordered without meat!! They have something for everyone here",
+ },
+ accessory: {
+ type: "image",
+ image_url:
+ "https://s3-media3.fl.yelpcdn.com/bphoto/c7ed05m9lC2EmA3Aruue7A/o.jpg",
+ alt_text: "alt text for image",
+ },
+ },
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: "*Kin Khao*\n:star::star::star::star: 1638 reviews\n The sticky rice also goes wonderfully with the caramelized pork belly, which is absolutely melt-in-your-mouth and so soft.",
+ },
+ accessory: {
+ type: "image",
+ image_url:
+ "https://s3-media2.fl.yelpcdn.com/bphoto/korel-1YjNtFtJlMTaC26A/o.jpg",
+ alt_text: "alt text for image",
+ },
+ },
+ {
+ type: "section",
+ text: {
+ type: "mrkdwn",
+ text: "*Ler Ros*\n:star::star::star::star: 2082 reviews\n I would really recommend the Yum Koh Moo Yang - Spicy lime dressing and roasted quick marinated pork shoulder, basil leaves, chili & rice powder.",
+ },
+ accessory: {
+ type: "image",
+ image_url:
+ "https://s3-media2.fl.yelpcdn.com/bphoto/DawwNigKJ2ckPeDeDM7jAg/o.jpg",
+ alt_text: "alt text for image",
+ },
+ },
+ {
+ type: "divider",
+ },
+ {
+ type: "actions",
+ elements: [
+ {
+ type: "button",
+ text: {
+ type: "plain_text",
+ text: "Farmhouse",
+ emoji: true,
+ },
+ value: "click_me_123",
+ },
+ {
+ type: "button",
+ text: {
+ type: "plain_text",
+ text: "Kin Khao",
+ emoji: true,
+ },
+ value: "click_me_123",
+ url: "https://google.com",
+ },
+ {
+ type: "button",
+ text: {
+ type: "plain_text",
+ text: "Ler Ros",
+ emoji: true,
+ },
+ value: "click_me_123",
+ url: "https://google.com",
+ },
+ ],
+ },
+ ],
});
await ctx.waitFor("initial-wait", { seconds: 5 });
@@ -36,4 +124,3 @@ new Trigger({
return {};
},
}).listen();
-
From 109d81d9fa6ae6b6d55d4aff14b3279a59057573 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Tue, 24 Jan 2023 21:53:02 -0800
Subject: [PATCH 08/39] =?UTF-8?q?Slack=20blocks=20schemas=20are=20finally?=
=?UTF-8?q?=20parsing=20properly=E2=80=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/providers/slack/blocks.ts | 342 ++++++++++--------
1 file changed, 195 insertions(+), 147 deletions(-)
diff --git a/packages/trigger-providers/src/providers/slack/blocks.ts b/packages/trigger-providers/src/providers/slack/blocks.ts
index ac7ab104b8a..514b9725df9 100644
--- a/packages/trigger-providers/src/providers/slack/blocks.ts
+++ b/packages/trigger-providers/src/providers/slack/blocks.ts
@@ -75,7 +75,16 @@ export const actionSchema = z.object({
type: z.string(),
/**
* @description: An identifier for this action. You can use this when you receive an interaction payload to
- * {@link https://api.slack.com/interactivity/handling#payloads identify the source of the action}. Should be unique
+ * {@link https://api.slack.com/interactivity/hmergeling#payloads identify the source of the action}. Should be unique
+ * among all other `action_id`s in the containing block. Maximum length for this field is 255 characters.
+ */
+ action_id: z.string().optional(),
+});
+
+export const actionIdSchema = z.object({
+ /**
+ * @description: An identifier for this action. You can use this when you receive an interaction payload to
+ * {@link https://api.slack.com/interactivity/hmergeling#payloads identify the source of the action}. Should be unique
* among all other `action_id`s in the containing block. Maximum length for this field is 255 characters.
*/
action_id: z.string().optional(),
@@ -114,30 +123,29 @@ export const dispatchableSchema = z.object({
dispatch_action_config: dispatchActionConfigSchema.optional(),
});
-export const usersSelectSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+export const usersSelectSchema = z
+ .object({
type: z.literal("users_select"),
initial_user: z.string().optional(),
- });
-
-export const multiUsersSelectSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const multiUsersSelectSchema = z
+ .object({
type: z.literal("multi_users_select"),
initial_users: z.array(z.string()).optional(),
max_selected_items: z.number().optional(),
- });
-
-export const staticSelectSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const staticSelectSchema = z
+ .object({
type: z.literal("static_select"),
initial_option: plainTextOptionSchema.optional(),
options: z.array(plainTextOptionSchema).optional(),
@@ -149,13 +157,14 @@ export const staticSelectSchema = actionSchema
})
)
.optional(),
- });
-
-export const multiStaticSelectSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const multiStaticSelectSchema = z
+ .object({
type: z.literal("multi_static_select"),
initial_options: z.array(plainTextOptionSchema).optional(),
options: z.array(plainTextOptionSchema).optional(),
@@ -168,13 +177,14 @@ export const multiStaticSelectSchema = actionSchema
)
.optional(),
max_selected_items: z.number().optional(),
- });
-
-export const conversationsSelectSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const conversationsSelectSchema = z
+ .object({
type: z.literal("conversations_select"),
initial_conversation: z.string().optional(),
response_url_enabled: z.boolean().optional(),
@@ -195,13 +205,14 @@ export const conversationsSelectSchema = actionSchema
exclude_bot_users: z.boolean().optional(),
})
.optional(),
- });
-
-export const multiConversationsSelectSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const multiConversationsSelectSchema = z
+ .object({
type: z.literal("multi_conversations_select"),
initial_conversations: z.array(z.string()).optional(),
max_selected_items: z.number().optional(),
@@ -222,128 +233,140 @@ export const multiConversationsSelectSchema = actionSchema
exclude_bot_users: z.boolean().optional(),
})
.optional(),
- });
-
-export const channelsSelectSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const channelsSelectSchema = z
+ .object({
type: z.literal("channels_select"),
initial_channel: z.string().optional(),
- });
-
-export const multiChannelsSelectSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const multiChannelsSelectSchema = z
+ .object({
type: z.literal("multi_channels_select"),
initial_channels: z.array(z.string()).optional(),
max_selected_items: z.number().optional(),
- });
-
-export const externalSelectSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const externalSelectSchema = z
+ .object({
type: z.literal("external_select"),
initial_option: plainTextOptionSchema.optional(),
min_query_length: z.number().optional(),
- });
-
-export const multiExternalSelectSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const multiExternalSelectSchema = z
+ .object({
type: z.literal("multi_external_select"),
initial_options: z.array(plainTextOptionSchema).optional(),
min_query_length: z.number().optional(),
max_selected_items: z.number().optional(),
- });
-
-export const buttonSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const buttonSchema = z
+ .object({
type: z.literal("button"),
text: plainTextElementSchema,
value: z.string().optional(),
url: z.string().optional(),
style: z.union([z.literal("danger"), z.literal("primary")]).optional(),
accessibility_label: z.string().optional(),
- });
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema);
-export const overflowSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend({
+export const overflowSchema = z
+ .object({
type: z.literal("overflow"),
options: z.array(plainTextOptionSchema),
- });
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema);
-export const datepickerSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+export const datepickerSchema = z
+ .object({
type: z.literal("datepicker"),
initial_date: z.string().optional(),
- });
-
-export const timepickerSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const timepickerSchema = z
+ .object({
type: z.literal("timepicker"),
initial_time: z.string().optional(),
timezone: z.string().optional(),
- });
-
-export const radioButtonsSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend({
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
+
+export const radioButtonsSchema = z
+ .object({
type: z.literal("radio_buttons"),
initial_option: optionSchema.optional(),
options: z.array(optionSchema),
- });
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema);
/**
* @description An element that allows the selection of a time of day formatted as a UNIX timestamp. On desktop
- * clients, this time picker will take the form of a dropdown list and the date picker will take the form of a dropdown
- * calendar. Both options will have free-text entry for precise choices. On mobile clients, the time picker and date
+ * clients, this time picker will take the form of a dropdown list merge the date picker will take the form of a dropdown
+ * calendar. Both options will have free-text entry for precise choices. On mobile clients, the time picker merge date
* picker will use native UIs.
* {@link https://api.slack.com/reference/block-kit/block-elements#datetimepicker}
*/
-export const dateTimepickerSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend({
+export const dateTimepickerSchema = z
+ .object({
type: z.literal("datetimepicker"),
/**
- * @description The initial date and time that is selected when the element is loaded, represented as a UNIX
- * timestamp in seconds. This should be in the format of 10 digits, for example 1628633820 represents the date and
+ * @description The initial date merge time that is selected when the element is loaded, represented as a UNIX
+ * timestamp in seconds. This should be in the format of 10 digits, for example 1628633820 represents the date merge
* time August 10th, 2021 at 03:17pm PST.
*/
initial_date_time: z.number().optional(),
- });
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema);
-export const checkboxesSchema = actionSchema
- .extend(confirmableSchema.shape)
- .extend(focusableSchema.shape)
- .extend({
+export const checkboxesSchema = z
+ .object({
type: z.literal("checkboxes"),
initial_options: z.array(optionSchema).optional(),
options: z.array(optionSchema),
- });
+ })
+ .merge(actionIdSchema)
+ .merge(confirmableSchema)
+ .merge(focusableSchema);
-export const plainTextInputSchema = actionSchema
- .extend(dispatchableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+export const plainTextInputSchema = z
+ .object({
type: z.literal("plain_text_input"),
initial_value: z.string().optional(),
multiline: z.boolean().optional(),
@@ -351,53 +374,56 @@ export const plainTextInputSchema = actionSchema
max_length: z.number().optional(),
dispatch_action_config: dispatchActionConfigSchema.optional(),
focus_on_load: z.boolean().optional(),
- });
+ })
+ .merge(actionIdSchema)
+ .merge(dispatchableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
/**
* @description A URL input element, similar to the {@see PlainTextInput} element, creates a single line field where
* a user can enter URL-encoded data.
* {@link https://api.slack.com/reference/block-kit/block-elements#url}
*/
-export const uRLInputSchema = actionSchema
- .extend(dispatchableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+export const uRLInputSchema = z
+ .object({
type: z.literal("url_text_input"),
/**
* @description The initial value in the URL input when it is loaded.
*/
initial_value: z.string().optional(),
- });
+ })
+ .merge(actionIdSchema)
+ .merge(dispatchableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
/**
* @description An email input element, similar to the {@see PlainTextInput} element, creates a single line field where
* a user can enter an email address.
* {@link https://api.slack.com/reference/block-kit/block-elements#email}
*/
-export const emailInputSchema = actionSchema
- .extend(dispatchableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+export const emailInputSchema = z
+ .object({
type: z.literal("email_text_input"),
/**
* @description The initial value in the email input when it is loaded.
*/
initial_value: z.string().optional(),
- });
+ })
+ .merge(actionIdSchema)
+ .merge(dispatchableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
/**
* @description A number input element, similar to the {@see PlainTextInput} element, creates a single line field where
- * a user can a number. This input elements accepts floating point numbers, for example, 0.25, 5.5, and -10 are all
+ * a user can a number. This input elements accepts floating point numbers, for example, 0.25, 5.5, merge -10 are all
* valid input values. Decimal numbers are only allowed when `is_decimal_allowed` is equal to `true`.
* {@link https://api.slack.com/reference/block-kit/block-elements#number}
*/
-export const numberInputSchema = actionSchema
- .extend(dispatchableSchema.shape)
- .extend(focusableSchema.shape)
- .extend(placeholdableSchema.shape)
- .extend({
+export const numberInputSchema = z
+ .object({
type: z.literal("number_input"),
/**
* @description Decimal numbers are allowed if this property is `true`, set the value to `false` otherwise.
@@ -415,7 +441,11 @@ export const numberInputSchema = actionSchema
* @description The maximum value, cannot be less than `min_value`.
*/
max_value: z.string().optional(),
- });
+ })
+ .merge(actionIdSchema)
+ .merge(dispatchableSchema)
+ .merge(focusableSchema)
+ .merge(placeholdableSchema);
export const blockSchema = z.object({
type: z.string(),
@@ -569,8 +599,11 @@ const selectSchemas = [
];
export const selectSchema = z.discriminatedUnion("type", [
- selectSchemas[0],
- ...selectSchemas.slice(1),
+ usersSelectSchema,
+ staticSelectSchema,
+ conversationsSelectSchema,
+ channelsSelectSchema,
+ externalSelectSchema,
]);
const multiSelectSchemas = [
@@ -581,8 +614,11 @@ const multiSelectSchemas = [
multiExternalSelectSchema,
];
export const multiSelectSchema = z.discriminatedUnion("type", [
- multiSelectSchemas[0],
- ...multiSelectSchemas.slice(1),
+ multiUsersSelectSchema,
+ multiStaticSelectSchema,
+ multiConversationsSelectSchema,
+ multiChannelsSelectSchema,
+ multiExternalSelectSchema,
]);
export const actionsBlockSchema = blockSchema.extend({
@@ -597,7 +633,6 @@ export const actionsBlockSchema = blockSchema.extend({
...selectSchemas,
radioButtonsSchema,
checkboxesSchema,
- actionSchema,
])
),
});
@@ -623,7 +658,6 @@ export const sectionBlockSchema = blockSchema.extend({
timepickerSchema,
...selectSchemas,
...multiSelectSchemas,
- actionSchema,
imageElementSchema,
radioButtonsSchema,
checkboxesSchema,
@@ -716,13 +750,27 @@ const knownBlocks = [
];
export const knownBlockSchema = z.discriminatedUnion("type", [
- knownBlocks[0],
- ...knownBlocks.slice(1),
+ imageBlockSchema,
+ contextBlockSchema,
+ actionsBlockSchema,
+ dividerBlockSchema,
+ sectionBlockSchema,
+ inputBlockSchema,
+ fileBlockSchema,
+ headerBlockSchema,
+ videoBlockSchema,
]);
const anyBlockSchema = z.discriminatedUnion("type", [
- blockSchema,
- ...knownBlocks,
+ imageBlockSchema,
+ contextBlockSchema,
+ actionsBlockSchema,
+ dividerBlockSchema,
+ sectionBlockSchema,
+ inputBlockSchema,
+ fileBlockSchema,
+ headerBlockSchema,
+ videoBlockSchema,
]);
export const messageAttachmentSchema = z.object({
From e607281b1bf6b51cf683f72860aeab0f15c12e70 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Tue, 24 Jan 2023 23:27:56 -0800
Subject: [PATCH 09/39] Pass Zod issues back from HTTPService, so they can be
displayed
---
.../internal-integrations/src/services/index.ts | 13 ++++++++++++-
packages/internal-integrations/src/slack/index.ts | 2 +-
.../src/providers/slack/schemas.ts | 2 +-
3 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/packages/internal-integrations/src/services/index.ts b/packages/internal-integrations/src/services/index.ts
index 16f4bd72517..512829b9465 100644
--- a/packages/internal-integrations/src/services/index.ts
+++ b/packages/internal-integrations/src/services/index.ts
@@ -9,7 +9,10 @@ export type HttpServiceOptions = {
baseUrl: string;
};
-export type HttpResponse =
+export type HttpResponse<
+ TResponseSchema extends z.ZodTypeAny,
+ TErrorResponseSchema extends z.ZodTypeAny = z.AnyZodObject
+> =
| {
success: true;
statusCode: number;
@@ -20,6 +23,7 @@ export type HttpResponse =
success: false;
statusCode: number;
headers: Record;
+ error?: z.infer;
};
export class HttpService {
@@ -79,6 +83,13 @@ export class HttpService {
parsedJson,
parsedJson.error
);
+
+ return {
+ success: false,
+ statusCode: response.status,
+ headers: normalizeHeaders(response.headers),
+ error: { type: "zod-parse", issues: parsedJson.error.issues },
+ };
}
return {
diff --git a/packages/internal-integrations/src/slack/index.ts b/packages/internal-integrations/src/slack/index.ts
index 94cd52d3374..e4c9a4b7792 100644
--- a/packages/internal-integrations/src/slack/index.ts
+++ b/packages/internal-integrations/src/slack/index.ts
@@ -136,7 +136,7 @@ class SlackRequestIntegration implements RequestIntegration {
ok: false,
isRetryable: this.#isRetryable(response.statusCode),
response: {
- output: {},
+ output: response.error,
context: {
statusCode: response.statusCode,
headers: response.headers,
diff --git a/packages/trigger-providers/src/providers/slack/schemas.ts b/packages/trigger-providers/src/providers/slack/schemas.ts
index dab6b8366fd..7e075949f9f 100644
--- a/packages/trigger-providers/src/providers/slack/schemas.ts
+++ b/packages/trigger-providers/src/providers/slack/schemas.ts
@@ -7,7 +7,7 @@ export const PostMessageSuccessResponseSchema = z.object({
ts: z.string(),
message: z.object({
text: z.string(),
- user: z.string(),
+ user: z.string().optional(),
bot_id: z.string(),
attachments: z.array(z.unknown()).optional(),
type: z.string(),
From bce1bd13146333b2779c8f4ec78234d186a789d1 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Tue, 24 Jan 2023 23:33:27 -0800
Subject: [PATCH 10/39] =?UTF-8?q?Example=20with=20a=20Slack=20post=20that?=
=?UTF-8?q?=20looks=20like=20it=E2=80=99s=20from=20someone=20else?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
examples/send-to-slack/src/index.ts | 24 +++++++++++++++++++++---
1 file changed, 21 insertions(+), 3 deletions(-)
diff --git a/examples/send-to-slack/src/index.ts b/examples/send-to-slack/src/index.ts
index f5186adf0b2..2efec4c8eba 100644
--- a/examples/send-to-slack/src/index.ts
+++ b/examples/send-to-slack/src/index.ts
@@ -116,11 +116,29 @@ new Trigger({
await ctx.waitFor("initial-wait", { seconds: 5 });
- const secondResponse = await slack.postMessage("send-to-slack-channel-id", {
- channelId: response.channel,
- text: `Sent using the channelId: ${response.channel}`,
+ await slack.postMessage("arnie", {
+ username: "Arnie",
+ icon_url:
+ "https://www.themoviedb.org/t/p/w500/zEMhugsgXIpnQqO31GpAJYMUZZ1.jpg",
+ channelName: "test-integrations",
+ text: getRandomQuote(),
});
return {};
},
}).listen();
+
+function getRandomQuote() {
+ const arnoldQuotes = [
+ "I'll be back.",
+ "Strength does not come from winning. Your struggles develop your strengths. When you go through hardships and decide not to surrender, that is strength.",
+ "The mind is the limit. As long as the mind can envision the fact that you can do something, you can do it, as long as you really believe 100 percent.",
+ "Success is not the key to happiness. Happiness is the key to success. If you love what you are doing, you will be successful.",
+ "For me life is continuously being hungry. The meaning of life is not simply to exist, to survive, but to move ahead, to go up, to achieve, to conquer.",
+ "The best activities for your health are pumping and humping.",
+ "I have a love interest in every one of my films: a gun.",
+ "You can have results or excuses, but not both.",
+ ];
+
+ return arnoldQuotes[Math.floor(Math.random() * arnoldQuotes.length)];
+}
From 764f5c1fbba35da0e13aa883e97168567ec2c115 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Tue, 24 Jan 2023 23:33:49 -0800
Subject: [PATCH 11/39] IntegrationRequest input code block has a max height on
it
---
.../workflows/$workflowSlug/runs/$runId.tsx | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx
index 0b143e9165c..6e144aff881 100644
--- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx
+++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx
@@ -678,7 +678,11 @@ function IntegrationRequestStep({
{request.input && (
<>
-
+
>
)}
From 33128bc124d7182a834eea462d77ef188591e656 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Wed, 25 Jan 2023 12:23:20 -0800
Subject: [PATCH 12/39] Move Slack requests to a separate file
---
.../internal-integrations/src/slack/index.ts | 247 +-----------------
.../src/slack/requests.ts | 246 +++++++++++++++++
2 files changed, 248 insertions(+), 245 deletions(-)
create mode 100644 packages/internal-integrations/src/slack/requests.ts
diff --git a/packages/internal-integrations/src/slack/index.ts b/packages/internal-integrations/src/slack/index.ts
index e4c9a4b7792..0f933af238c 100644
--- a/packages/internal-integrations/src/slack/index.ts
+++ b/packages/internal-integrations/src/slack/index.ts
@@ -1,246 +1,3 @@
-import { HttpEndpoint, HttpService } from "../services";
-import {
- DisplayProperties,
- CacheService,
- PerformedRequestResponse,
- PerformRequestOptions,
- RequestIntegration,
- AccessInfo,
-} from "../types";
-import { slack } from "@trigger.dev/providers";
-import debug from "debug";
-import { getAccessToken } from "../accessInfo";
-import { z } from "zod";
-import type { ReactNode } from "react";
+import { requests } from "./requests";
-const log = debug("trigger:integrations:slack");
-
-const SendSlackMessageRequestBodySchema =
- slack.schemas.PostMessageBodySchema.extend({
- link_names: z.literal(1),
- });
-
-class SlackRequestIntegration implements RequestIntegration {
- #joinChannelEndpoint = new HttpEndpoint<
- typeof slack.schemas.JoinConversationResponseSchema,
- typeof slack.schemas.JoinConversationBodySchema
- >({
- response: slack.schemas.JoinConversationResponseSchema,
- method: "POST",
- path: "/conversations.join",
- });
-
- #listConversationsEndpoint = new HttpEndpoint({
- response: slack.schemas.ListConversationsResponseSchema,
- method: "GET",
- path: "/conversations.list",
- });
-
- #postMessageEndpoint = new HttpEndpoint<
- typeof slack.schemas.PostMessageResponseSchema,
- typeof SendSlackMessageRequestBodySchema
- >({
- response: slack.schemas.PostMessageResponseSchema,
- method: "POST",
- path: "/chat.postMessage",
- });
-
- constructor(private readonly baseUrl: string = "https://slack.com/api") {}
-
- perform(options: PerformRequestOptions): Promise {
- switch (options.endpoint) {
- case "chat.postMessage": {
- return this.#postMessage(
- options.accessInfo,
- options.params,
- options.cache
- );
- }
- default: {
- throw new Error(`Unknown endpoint: ${options.endpoint}`);
- }
- }
- }
-
- displayProperties(endpoint: string, params: any): DisplayProperties {
- switch (endpoint) {
- case "chat.postMessage": {
- return {
- title: `Post message to ${
- "channelName" in params ? params.channelName : params.channelId
- }`,
- properties: [
- {
- key: "Text",
- value: params.text,
- },
- ],
- };
- }
- default: {
- throw new Error(`Unknown endpoint: ${endpoint}`);
- }
- }
- }
-
- renderComponent(input: any, output: any): ReactNode {
- return null;
- }
-
- async #postMessage(
- accessInfo: AccessInfo,
- params: any,
- cache?: CacheService
- ): Promise {
- const parsedParams = slack.schemas.PostMessageOptionsSchema.parse(params);
-
- log("chat.postMessage %O", parsedParams);
-
- const accessToken = getAccessToken(accessInfo);
-
- const service = new HttpService({
- accessToken,
- baseUrl: this.baseUrl,
- });
-
- const channelId = await this.#findChannelId(service, params, cache);
-
- if (!channelId) {
- return {
- ok: false,
- isRetryable: false,
- response: {
- output: {
- message: `channelId not found`,
- },
- context: {
- statusCode: 404,
- headers: {},
- },
- },
- };
- }
-
- log("found channelId %s", channelId);
-
- const response = await service.performRequest(this.#postMessageEndpoint, {
- ...parsedParams,
- link_names: 1,
- channel: channelId,
- });
-
- if (!response.success) {
- log("chat.postMessage failed %O", response);
-
- return {
- ok: false,
- isRetryable: this.#isRetryable(response.statusCode),
- response: {
- output: response.error,
- context: {
- statusCode: response.statusCode,
- headers: response.headers,
- },
- },
- };
- }
-
- if (!response.data.ok && response.data.error === "not_in_channel") {
- log(
- "chat.postMessage failed with not_in_channel, attempting to join channel %s",
- channelId
- );
-
- // Attempt to join the channel, and then retry the request
- const joinResponse = await service.performRequest(
- this.#joinChannelEndpoint,
- {
- channel: channelId,
- }
- );
-
- if (joinResponse.success && joinResponse.data.ok) {
- log("joined channel %s, retrying postMessage", channelId);
-
- return this.#postMessage(accessInfo, params);
- }
- }
-
- const ok = response.data.ok;
-
- const performedRequest = {
- ok,
- isRetryable: this.#isRetryable(response.statusCode),
- response: {
- output: response.data,
- context: {
- statusCode: response.statusCode,
- headers: response.headers,
- },
- },
- };
-
- log("chat.postMessage performedRequest %O", performedRequest);
-
- return performedRequest;
- }
-
- #isRetryable(statusCode: number): boolean {
- return (
- statusCode === 408 ||
- statusCode === 429 ||
- statusCode === 500 ||
- statusCode === 502 ||
- statusCode === 503 ||
- statusCode === 504
- );
- }
-
- // Will use the conversations.list API (using fetch) to find the channel ID
- // unless the channel is already provided in the format of a channelID (for example: "D8572TUFR" or "C01BQJZLJGZ")
- async #findChannelId(
- service: HttpService,
- params: z.infer,
- cache?: CacheService
- ): Promise {
- if ("channelId" in params) {
- return params.channelId;
- }
-
- if (!("channelName" in params)) {
- throw new Error("Invalid params, mising channelId and channelName");
- }
-
- //if the channelName starts with a #, remove it
- if (params.channelName.startsWith("#")) {
- params.channelName = params.channelName.substring(1);
- }
-
- const cachedChannelId = await cache?.get(params.channelName);
-
- if (cachedChannelId) {
- return cachedChannelId;
- }
-
- const response = await service.performRequest(
- this.#listConversationsEndpoint
- );
-
- if (response.success && response.data.ok) {
- const { channels } = response.data;
-
- const channelInfo = channels.find(
- (c: any) => c.name === params.channelName
- );
-
- if (channelInfo) {
- await cache?.set(params.channelName, channelInfo.id, 60 * 60 * 24);
- return channelInfo.id;
- }
- }
-
- return undefined;
- }
-}
-
-export const requests = new SlackRequestIntegration();
+export { requests };
diff --git a/packages/internal-integrations/src/slack/requests.ts b/packages/internal-integrations/src/slack/requests.ts
new file mode 100644
index 00000000000..e4c9a4b7792
--- /dev/null
+++ b/packages/internal-integrations/src/slack/requests.ts
@@ -0,0 +1,246 @@
+import { HttpEndpoint, HttpService } from "../services";
+import {
+ DisplayProperties,
+ CacheService,
+ PerformedRequestResponse,
+ PerformRequestOptions,
+ RequestIntegration,
+ AccessInfo,
+} from "../types";
+import { slack } from "@trigger.dev/providers";
+import debug from "debug";
+import { getAccessToken } from "../accessInfo";
+import { z } from "zod";
+import type { ReactNode } from "react";
+
+const log = debug("trigger:integrations:slack");
+
+const SendSlackMessageRequestBodySchema =
+ slack.schemas.PostMessageBodySchema.extend({
+ link_names: z.literal(1),
+ });
+
+class SlackRequestIntegration implements RequestIntegration {
+ #joinChannelEndpoint = new HttpEndpoint<
+ typeof slack.schemas.JoinConversationResponseSchema,
+ typeof slack.schemas.JoinConversationBodySchema
+ >({
+ response: slack.schemas.JoinConversationResponseSchema,
+ method: "POST",
+ path: "/conversations.join",
+ });
+
+ #listConversationsEndpoint = new HttpEndpoint({
+ response: slack.schemas.ListConversationsResponseSchema,
+ method: "GET",
+ path: "/conversations.list",
+ });
+
+ #postMessageEndpoint = new HttpEndpoint<
+ typeof slack.schemas.PostMessageResponseSchema,
+ typeof SendSlackMessageRequestBodySchema
+ >({
+ response: slack.schemas.PostMessageResponseSchema,
+ method: "POST",
+ path: "/chat.postMessage",
+ });
+
+ constructor(private readonly baseUrl: string = "https://slack.com/api") {}
+
+ perform(options: PerformRequestOptions): Promise {
+ switch (options.endpoint) {
+ case "chat.postMessage": {
+ return this.#postMessage(
+ options.accessInfo,
+ options.params,
+ options.cache
+ );
+ }
+ default: {
+ throw new Error(`Unknown endpoint: ${options.endpoint}`);
+ }
+ }
+ }
+
+ displayProperties(endpoint: string, params: any): DisplayProperties {
+ switch (endpoint) {
+ case "chat.postMessage": {
+ return {
+ title: `Post message to ${
+ "channelName" in params ? params.channelName : params.channelId
+ }`,
+ properties: [
+ {
+ key: "Text",
+ value: params.text,
+ },
+ ],
+ };
+ }
+ default: {
+ throw new Error(`Unknown endpoint: ${endpoint}`);
+ }
+ }
+ }
+
+ renderComponent(input: any, output: any): ReactNode {
+ return null;
+ }
+
+ async #postMessage(
+ accessInfo: AccessInfo,
+ params: any,
+ cache?: CacheService
+ ): Promise {
+ const parsedParams = slack.schemas.PostMessageOptionsSchema.parse(params);
+
+ log("chat.postMessage %O", parsedParams);
+
+ const accessToken = getAccessToken(accessInfo);
+
+ const service = new HttpService({
+ accessToken,
+ baseUrl: this.baseUrl,
+ });
+
+ const channelId = await this.#findChannelId(service, params, cache);
+
+ if (!channelId) {
+ return {
+ ok: false,
+ isRetryable: false,
+ response: {
+ output: {
+ message: `channelId not found`,
+ },
+ context: {
+ statusCode: 404,
+ headers: {},
+ },
+ },
+ };
+ }
+
+ log("found channelId %s", channelId);
+
+ const response = await service.performRequest(this.#postMessageEndpoint, {
+ ...parsedParams,
+ link_names: 1,
+ channel: channelId,
+ });
+
+ if (!response.success) {
+ log("chat.postMessage failed %O", response);
+
+ return {
+ ok: false,
+ isRetryable: this.#isRetryable(response.statusCode),
+ response: {
+ output: response.error,
+ context: {
+ statusCode: response.statusCode,
+ headers: response.headers,
+ },
+ },
+ };
+ }
+
+ if (!response.data.ok && response.data.error === "not_in_channel") {
+ log(
+ "chat.postMessage failed with not_in_channel, attempting to join channel %s",
+ channelId
+ );
+
+ // Attempt to join the channel, and then retry the request
+ const joinResponse = await service.performRequest(
+ this.#joinChannelEndpoint,
+ {
+ channel: channelId,
+ }
+ );
+
+ if (joinResponse.success && joinResponse.data.ok) {
+ log("joined channel %s, retrying postMessage", channelId);
+
+ return this.#postMessage(accessInfo, params);
+ }
+ }
+
+ const ok = response.data.ok;
+
+ const performedRequest = {
+ ok,
+ isRetryable: this.#isRetryable(response.statusCode),
+ response: {
+ output: response.data,
+ context: {
+ statusCode: response.statusCode,
+ headers: response.headers,
+ },
+ },
+ };
+
+ log("chat.postMessage performedRequest %O", performedRequest);
+
+ return performedRequest;
+ }
+
+ #isRetryable(statusCode: number): boolean {
+ return (
+ statusCode === 408 ||
+ statusCode === 429 ||
+ statusCode === 500 ||
+ statusCode === 502 ||
+ statusCode === 503 ||
+ statusCode === 504
+ );
+ }
+
+ // Will use the conversations.list API (using fetch) to find the channel ID
+ // unless the channel is already provided in the format of a channelID (for example: "D8572TUFR" or "C01BQJZLJGZ")
+ async #findChannelId(
+ service: HttpService,
+ params: z.infer,
+ cache?: CacheService
+ ): Promise {
+ if ("channelId" in params) {
+ return params.channelId;
+ }
+
+ if (!("channelName" in params)) {
+ throw new Error("Invalid params, mising channelId and channelName");
+ }
+
+ //if the channelName starts with a #, remove it
+ if (params.channelName.startsWith("#")) {
+ params.channelName = params.channelName.substring(1);
+ }
+
+ const cachedChannelId = await cache?.get(params.channelName);
+
+ if (cachedChannelId) {
+ return cachedChannelId;
+ }
+
+ const response = await service.performRequest(
+ this.#listConversationsEndpoint
+ );
+
+ if (response.success && response.data.ok) {
+ const { channels } = response.data;
+
+ const channelInfo = channels.find(
+ (c: any) => c.name === params.channelName
+ );
+
+ if (channelInfo) {
+ await cache?.set(params.channelName, channelInfo.id, 60 * 60 * 24);
+ return channelInfo.id;
+ }
+ }
+
+ return undefined;
+ }
+}
+
+export const requests = new SlackRequestIntegration();
From 31a8e364de4635bceb5789cbe20d37bf9a493c16 Mon Sep 17 00:00:00 2001
From: D-K-P
Date: Thu, 26 Jan 2023 11:44:59 -0800
Subject: [PATCH 13/39] Edited YT iframe and added blurb to the resend examples
box
---
apps/docs/examples/examples.mdx | 4 +++-
apps/docs/welcome.mdx | 8 ++++----
2 files changed, 7 insertions(+), 5 deletions(-)
diff --git a/apps/docs/examples/examples.mdx b/apps/docs/examples/examples.mdx
index c8196f5e109..e1addc36d8c 100644
--- a/apps/docs/examples/examples.mdx
+++ b/apps/docs/examples/examples.mdx
@@ -17,5 +17,7 @@ You can use these workflows in your product by following the instructions on eac
Post to Slack when a GitHub issue is created or modified.
-
+
+ Create a welcome email drip campaign.
+
diff --git a/apps/docs/welcome.mdx b/apps/docs/welcome.mdx
index 98c6b3043e9..816a75828ea 100644
--- a/apps/docs/welcome.mdx
+++ b/apps/docs/welcome.mdx
@@ -12,15 +12,15 @@ _A workflow in action:_

-
+
Quickly get up and running with Trigger.dev by following our quick start
From 1a20d1b2a3f3eb651ceb790f82adea734f59ef2d Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Thu, 26 Jan 2023 12:53:49 -0800
Subject: [PATCH 14/39] Added explaination on how to send an event using cURL
to the docs
---
apps/docs/functions/send-event.mdx | 33 ++++++++++++++++++++++++++++--
1 file changed, 31 insertions(+), 2 deletions(-)
diff --git a/apps/docs/functions/send-event.mdx b/apps/docs/functions/send-event.mdx
index b709fcaaae7..bb299ac2151 100644
--- a/apps/docs/functions/send-event.mdx
+++ b/apps/docs/functions/send-event.mdx
@@ -4,9 +4,12 @@ sidebarTitle: "Send event"
description: "Send events to trigger your custom events workflows."
---
-## How to send an event
+## Send an event
-You can send an event from anywhere in your code, including from inside another workflow.
+You can easily send an event from anywhere, including from inside another workflow. Events don't have to come from the same server as your workflow and can be sent as HTTP requests from any language (see our cURL example below for the format).
+
+
+
```ts
import { sendEvent } from "@trigger.dev/sdk";
@@ -19,6 +22,28 @@ await sendEvent("start-fire-2", {
});
```
+
+
+
+```bash
+curl --request POST \
+ --url https://app.trigger.dev/api/v1/events \
+ --header 'Authorization: Bearer ' \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "id": "",
+ "event": {
+ "name": "user.created",
+ "payload": {
+ "userId": "123456"
+ }
+ }
+ }'
+```
+
+
+
+
If you are calling this from inside a workflow, ensure that the first parameter is unique inside your workflow.
## Sending events from other workflows
@@ -28,3 +53,7 @@ It is useful to send events from workflows because you can split your logic into
## Writing a workflow that is triggered by an event
You should read [the documentation on custom events](/triggers/custom-events) to learn how to write a workflow that is triggered by an event.
+
+```
+
+```
From 518472fcb2fc930a9510c361dc52a4398f26a2ba Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Thu, 26 Jan 2023 13:02:08 -0800
Subject: [PATCH 15/39] Fix for page count being wrong on runs table
---
apps/webapp/app/models/workflowRunListPresenter.server.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/apps/webapp/app/models/workflowRunListPresenter.server.ts b/apps/webapp/app/models/workflowRunListPresenter.server.ts
index 470c5b8aad7..e9fa5e4e0d3 100644
--- a/apps/webapp/app/models/workflowRunListPresenter.server.ts
+++ b/apps/webapp/app/models/workflowRunListPresenter.server.ts
@@ -54,6 +54,14 @@ export class WorkflowRunListPresenter {
where: {
workflow: {
slug: workflowSlug,
+ organization: {
+ slug: organizationSlug,
+ users: {
+ some: {
+ id: userId,
+ },
+ },
+ },
},
environment: {
slug: environmentSlug,
From a4bd86fefe40e63c4ca35d5e6655d52764d110c2 Mon Sep 17 00:00:00 2001
From: James Ritchie
Date: Thu, 26 Jan 2023 13:04:36 -0800
Subject: [PATCH 16/39] =?UTF-8?q?Added=20a=20typeform=20to=20vote=20for=20?=
=?UTF-8?q?integrations=20we=20don=E2=80=99t=20support?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../app/components/CreateNewWorkflow.tsx | 25 +++++++++++++++----
1 file changed, 20 insertions(+), 5 deletions(-)
diff --git a/apps/webapp/app/components/CreateNewWorkflow.tsx b/apps/webapp/app/components/CreateNewWorkflow.tsx
index 92961e3930b..f93612ba9a8 100644
--- a/apps/webapp/app/components/CreateNewWorkflow.tsx
+++ b/apps/webapp/app/components/CreateNewWorkflow.tsx
@@ -84,14 +84,22 @@ export function CreateNewWorkflowNoWorkflows() {
Easily authenticate with APIs using the supported integrations
below. If there's an integration we don't yet support,{" "}
-
+ {" "}
+ vote for it here
+ {" "}
and we'll add it.
+
{getProviders(false).map((provider) => (
Fetch
&
+ Webhooks
+
Join the community
From e5530299dac364e0ee5804d277374d7342537092 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Thu, 26 Jan 2023 13:07:14 -0800
Subject: [PATCH 17/39] Scope the test data to the correct organization
---
apps/webapp/app/models/workflowRun.server.ts | 5 +++++
.../$organizationSlug/workflows/$workflowSlug/test.tsx | 8 ++++++--
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/apps/webapp/app/models/workflowRun.server.ts b/apps/webapp/app/models/workflowRun.server.ts
index aaa6194939c..77c5d976b68 100644
--- a/apps/webapp/app/models/workflowRun.server.ts
+++ b/apps/webapp/app/models/workflowRun.server.ts
@@ -228,13 +228,18 @@ async function findWorkflowRunScopedToApiKey(id: string, apiKey: string) {
export async function getMostRecentWorkflowRun({
workflowSlug,
+ organizationSlug,
}: {
workflowSlug: string;
+ organizationSlug: string;
}) {
return prisma.workflowRun.findFirst({
where: {
workflow: {
slug: workflowSlug,
+ organization: {
+ slug: organizationSlug,
+ },
},
},
include: {
diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx
index f17914345af..f2ba3ac848d 100644
--- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx
+++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/test.tsx
@@ -18,11 +18,15 @@ import { requireUserId } from "~/services/session.server";
export const loader = async ({ request, params }: LoaderArgs) => {
await requireUserId(request);
- const { workflowSlug } = params;
+ const { workflowSlug, organizationSlug } = params;
invariant(workflowSlug, "workflowSlug is required");
+ invariant(organizationSlug, "organizationSlug is required");
try {
- const latestRun = await getMostRecentWorkflowRun({ workflowSlug });
+ const latestRun = await getMostRecentWorkflowRun({
+ workflowSlug,
+ organizationSlug,
+ });
return typedjson({ latestRun });
} catch (error: any) {
console.error(error);
From 86c543c45f726d146b1885a9c733480228f4e23f Mon Sep 17 00:00:00 2001
From: Eric Allam
Date: Thu, 26 Jan 2023 23:29:05 +0000
Subject: [PATCH 18/39] Fire off internal custom events for user.created and
workflow.created
---
apps/webapp/app/env.server.ts | 1 +
apps/webapp/app/services/emailAuth.server.tsx | 19 ++++++++------
.../app/services/events/ingest.server.ts | 13 ++++++++--
apps/webapp/app/services/gitHubAuth.server.ts | 11 ++++++++
.../app/services/messageBroker.server.ts | 25 +++++++++++++++++++
.../workflows/registerWorkflow.server.ts | 22 ++++++++++++++++
flightcontrol.json | 8 +++---
7 files changed, 86 insertions(+), 13 deletions(-)
diff --git a/apps/webapp/app/env.server.ts b/apps/webapp/app/env.server.ts
index 43d8bde6a3c..dda42842f76 100644
--- a/apps/webapp/app/env.server.ts
+++ b/apps/webapp/app/env.server.ts
@@ -40,6 +40,7 @@ const EnvironmentSchema = z.object({
PULSAR_ISSUER_URL: z.string().optional(),
PULSAR_AUDIENCE: z.string().optional(),
PULSAR_DEBUG: z.string().optional(),
+ INTERNAL_TRIGGER_API_KEY: z.string().optional(),
});
export type Environment = z.infer;
diff --git a/apps/webapp/app/services/emailAuth.server.tsx b/apps/webapp/app/services/emailAuth.server.tsx
index 83785dff738..23af9558570 100644
--- a/apps/webapp/app/services/emailAuth.server.tsx
+++ b/apps/webapp/app/services/emailAuth.server.tsx
@@ -6,6 +6,7 @@ import { findOrCreateUser } from "~/models/user.server";
import { env } from "~/env.server";
import { createFirstOrganization } from "~/models/organization.server";
import { sendMagicLinkEmail } from "~/services/email.server";
+import { taskQueue } from "./messageBroker.server";
let secret = env.MAGIC_LINK_SECRET;
if (!secret) throw new Error("Missing MAGIC_LINK_SECRET env variable.");
@@ -32,15 +33,19 @@ const emailStrategy = new EmailLinkStrategy(
authenticationMethod: "MAGIC_LINK",
});
- console.log(
- `User ${user.id} logged in with magic link. ${
- isNewUser ? "New user." : ""
- }`
- );
-
if (isNewUser) {
await createFirstOrganization(user);
- emailProvider.scheduleWelcomeEmail(user);
+ await emailProvider.scheduleWelcomeEmail(user);
+
+ await taskQueue.publish("SEND_INTERNAL_EVENT", {
+ id: user.id,
+ name: "user.created",
+ payload: {
+ id: user.id,
+ source: "MAGIC_LINK",
+ admin: user.admin,
+ },
+ });
}
return { userId: user.id };
diff --git a/apps/webapp/app/services/events/ingest.server.ts b/apps/webapp/app/services/events/ingest.server.ts
index b3ba11f1ccf..6ed3c602020 100644
--- a/apps/webapp/app/services/events/ingest.server.ts
+++ b/apps/webapp/app/services/events/ingest.server.ts
@@ -25,20 +25,29 @@ export class IngestEvent {
this.#prismaClient = prismaClient;
}
- public async call(options: IngestEventOptions, organization: Organization) {
+ public async call(options: IngestEventOptions, organization?: Organization) {
const environment = options.apiKey
? await findEnvironmentByApiKey(options.apiKey)
: undefined;
+ if (!environment && !organization) {
+ return {
+ status: "error" as const,
+ error: "No organization or environment found",
+ };
+ }
+
const id = options.id ?? ulid();
+ const organizationId = organization?.id ?? environment?.organizationId;
+
// Create a new event in the database
const event = await this.#prismaClient.triggerEvent.create({
data: {
id,
organization: {
connect: {
- id: organization.id,
+ id: organizationId,
},
},
environment: environment
diff --git a/apps/webapp/app/services/gitHubAuth.server.ts b/apps/webapp/app/services/gitHubAuth.server.ts
index a32f3af627e..7b5b1a5cc57 100644
--- a/apps/webapp/app/services/gitHubAuth.server.ts
+++ b/apps/webapp/app/services/gitHubAuth.server.ts
@@ -5,6 +5,7 @@ import { createFirstOrganization } from "~/models/organization.server";
import { findOrCreateUser } from "~/models/user.server";
import type { AuthUser } from "./authUser";
import { scheduleWelcomeEmail } from "./email.server";
+import { taskQueue } from "./messageBroker.server";
const gitHubStrategy = new GitHubStrategy(
{
@@ -31,6 +32,16 @@ const gitHubStrategy = new GitHubStrategy(
if (isNewUser) {
await createFirstOrganization(user);
await scheduleWelcomeEmail(user);
+
+ await taskQueue.publish("SEND_INTERNAL_EVENT", {
+ id: user.id,
+ name: "user.created",
+ payload: {
+ id: user.id,
+ source: "GITHUB",
+ admin: user.admin,
+ },
+ });
}
return {
diff --git a/apps/webapp/app/services/messageBroker.server.ts b/apps/webapp/app/services/messageBroker.server.ts
index 2e504053017..ddff46a03e6 100644
--- a/apps/webapp/app/services/messageBroker.server.ts
+++ b/apps/webapp/app/services/messageBroker.server.ts
@@ -1,5 +1,6 @@
import type { FetchOutputSchema } from "@trigger.dev/common-schemas";
import {
+ CustomEventSchema,
JsonSchema,
ScheduledEventPayloadSchema,
} from "@trigger.dev/common-schemas";
@@ -34,6 +35,7 @@ import { InitiateDelay } from "./delays/initiateDelay.server";
import { ResolveDelay } from "./delays/resolveDelay.server";
import { sendEmail } from "./email.server";
import { DispatchEvent } from "./events/dispatch.server";
+import { IngestEvent } from "./events/ingest.server";
import { HandleNewServiceConnection } from "./externalServices/handleNewConnection.server";
import { RegisterExternalSource } from "./externalSources/registerExternalSource.server";
import { CreateFetchRequest } from "./fetches/createFetchRequest.server";
@@ -419,6 +421,10 @@ const taskQueueCatalog = {
}),
properties: z.object({}),
},
+ SEND_INTERNAL_EVENT: {
+ data: CustomEventSchema.extend({ id: z.string() }),
+ properties: z.object({}),
+ },
};
function createTaskQueue() {
@@ -679,6 +685,25 @@ function createTaskQueue() {
return service.call(data.externalSourceId, data.payload);
},
+ SEND_INTERNAL_EVENT: async (id, data, properties, attributes) => {
+ if (attributes.redeliveryCount >= 4) {
+ return true;
+ }
+
+ const service = new IngestEvent();
+
+ const result = await service.call({
+ id: data.id,
+ name: data.name,
+ type: "CUSTOM_EVENT",
+ service: "trigger",
+ payload: data.payload,
+ context: data.context,
+ apiKey: env.INTERNAL_TRIGGER_API_KEY,
+ });
+
+ return result.status === "success";
+ },
},
});
diff --git a/apps/webapp/app/services/workflows/registerWorkflow.server.ts b/apps/webapp/app/services/workflows/registerWorkflow.server.ts
index d4b027d629c..750819cec73 100644
--- a/apps/webapp/app/services/workflows/registerWorkflow.server.ts
+++ b/apps/webapp/app/services/workflows/registerWorkflow.server.ts
@@ -99,6 +99,18 @@ export class RegisterWorkflow {
payload: WorkflowMetadata,
organization: Organization
) {
+ const existingWorkflow = await this.#prismaClient.workflow.findUnique({
+ where: {
+ organizationId_slug: {
+ organizationId: organization.id,
+ slug,
+ },
+ },
+ select: {
+ id: true,
+ },
+ });
+
const workflow = await this.#prismaClient.workflow.upsert({
where: {
organizationId_slug: {
@@ -130,6 +142,16 @@ export class RegisterWorkflow {
},
});
+ if (!existingWorkflow) {
+ await taskQueue.publish("SEND_INTERNAL_EVENT", {
+ id: workflow.id,
+ name: "workflow.created",
+ payload: {
+ id: workflow.id,
+ },
+ });
+ }
+
return workflow;
}
diff --git a/flightcontrol.json b/flightcontrol.json
index c9d599df4ce..b2f3d87451a 100644
--- a/flightcontrol.json
+++ b/flightcontrol.json
@@ -107,6 +107,9 @@
"fromParameterStore": "/Prod/webapp/POSTHOG_PROJECT_KEY"
},
"TRIGGER_LOG_LEVEL": "debug",
+ "INTERNAL_TRIGGER_API_KEY": {
+ "fromParameterStore": "/Prod/webapp/INTERNAL_TRIGGER_API_KEY"
+ },
"PULSAR_ENABLED": "1",
"PULSAR_DEBUG": true
}
@@ -147,10 +150,7 @@
"fromParameterStore": "/Prod/Pulsar/wss/Role"
},
"PLATFORM_API_URL": "https://app.trigger.dev",
- "TRIGGER_LOG_LEVEL": "debug",
- "TRIGGER_API_KEY": {
- "fromParameterStore": "/Prod/webapp/TRIGGER_API_KEY"
- }
+ "TRIGGER_LOG_LEVEL": "debug"
}
},
{
From 7e7c8dafd7de3ef3f3211a403e39ce3d432684c2 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Fri, 27 Jan 2023 00:24:19 -0800
Subject: [PATCH 19/39] Slack interactivity hook responds with 200
---
.../internal/webhooks/slack/interactivity.ts | 5 +
apps/webapp/app/utils/formData.ts | 10 ++
.../src/providers/slack/interactivity.ts | 111 ++++++++++++++++++
3 files changed, 126 insertions(+)
create mode 100644 apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts
create mode 100644 apps/webapp/app/utils/formData.ts
create mode 100644 packages/trigger-providers/src/providers/slack/interactivity.ts
diff --git a/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts b/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts
new file mode 100644
index 00000000000..c9798e7de10
--- /dev/null
+++ b/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts
@@ -0,0 +1,5 @@
+import type { ActionArgs } from "@remix-run/server-runtime";
+
+export async function action({ request }: ActionArgs) {
+ return { status: 200 };
+}
diff --git a/apps/webapp/app/utils/formData.ts b/apps/webapp/app/utils/formData.ts
new file mode 100644
index 00000000000..8290365263a
--- /dev/null
+++ b/apps/webapp/app/utils/formData.ts
@@ -0,0 +1,10 @@
+import type { z } from "zod";
+
+export function formDataAsObject(
+ formData: FormData,
+ schema: z.ZodSchema
+): TValues {
+ const object = Object.fromEntries(formData.entries());
+ const parsed = schema.parse(object);
+ return parsed;
+}
diff --git a/packages/trigger-providers/src/providers/slack/interactivity.ts b/packages/trigger-providers/src/providers/slack/interactivity.ts
new file mode 100644
index 00000000000..1bb3f018262
--- /dev/null
+++ b/packages/trigger-providers/src/providers/slack/interactivity.ts
@@ -0,0 +1,111 @@
+import { z } from "zod";
+import { plainTextElementSchema } from "./blocks";
+
+const blockActionType = z.union([
+ z.literal("block_actions"),
+ z.literal("interactive_message"),
+]);
+
+const sourceType = z.literal("message");
+
+export const blockAction = z.object({
+ type: blockActionType,
+ user: z.object({
+ id: z.string(),
+ username: z.string(),
+ name: z.string(),
+ team_id: z.string(),
+ }),
+ api_app_id: z.string(),
+ container: z.object({
+ type: sourceType,
+ message_ts: z.string(),
+ channel_id: z.string(),
+ is_ephemeral: z.boolean(),
+ }),
+ trigger_id: z.string(),
+ team: z.object({ id: z.string(), domain: z.string() }),
+ enterprise: z.null(),
+ is_enterprise_install: z.boolean(),
+ channel: z.object({ id: z.string(), name: z.string() }),
+ message: z
+ .object({
+ bot_id: z.string(),
+ type: sourceType,
+ text: z.string(),
+ user: z.string(),
+ ts: z.string(),
+ app_id: z.string(),
+ blocks: z.array(
+ z.union([
+ z.object({
+ type: z.string(),
+ block_id: z.string(),
+ text: z.object({
+ type: z.string(),
+ text: z.string(),
+ verbatim: z.boolean(),
+ }),
+ }),
+ z.object({ type: z.string(), block_id: z.string() }),
+ z.object({
+ type: z.string(),
+ block_id: z.string(),
+ text: z.object({
+ type: z.string(),
+ text: z.string(),
+ verbatim: z.boolean(),
+ }),
+ accessory: z.object({
+ type: z.string(),
+ image_url: z.string(),
+ alt_text: z.string(),
+ }),
+ }),
+ z.object({
+ type: z.string(),
+ block_id: z.string(),
+ elements: z.array(
+ z.union([
+ z.object({
+ type: z.string(),
+ action_id: z.string(),
+ text: z.object({
+ type: z.string(),
+ text: z.string(),
+ emoji: z.boolean(),
+ }),
+ value: z.string(),
+ }),
+ z.object({
+ type: z.string(),
+ action_id: z.string(),
+ text: z.object({
+ type: z.string(),
+ text: z.string(),
+ emoji: z.boolean(),
+ }),
+ value: z.string(),
+ url: z.string(),
+ }),
+ ])
+ ),
+ }),
+ ])
+ ),
+ team: z.string(),
+ })
+ .optional(),
+ state: z.object({ values: z.object({}) }),
+ response_url: z.string(),
+ actions: z.array(
+ z.object({
+ action_id: z.string(),
+ block_id: z.string(),
+ text: plainTextElementSchema,
+ value: z.string(),
+ type: z.string(),
+ action_ts: z.string(),
+ })
+ ),
+});
From 52d21ac84df6d001b904864d9a677b36b662ccdc Mon Sep 17 00:00:00 2001
From: Eric Allam
Date: Fri, 27 Jan 2023 10:18:13 +0000
Subject: [PATCH 20/39] Added support for delaying delivery when sending custom
events
---
.changeset/cuddly-ligers-reflect.md | 5 +
apps/docs/functions/send-event.mdx | 150 +++++++++++++++-
apps/docs/mint.json | 3 +-
apps/docs/reference/send-event.mdx | 97 +++++++++++
apps/webapp/app/models/workflowRun.server.ts | 30 ++--
.../workflows/$workflowSlug/runs/$runId.tsx | 36 +++-
apps/webapp/app/routes/api/v1/events.ts | 27 ++-
.../$organizationSlug/test/$workflowSlug.ts | 24 ++-
.../events/ingestCustomEvent.server.ts | 60 +++++++
.../app/services/messageBroker.server.ts | 38 +++-
apps/webapp/app/utils/json.ts | 7 +
apps/webapp/app/utils/objects.ts | 14 ++
examples/smoke-test/src/index.ts | 20 +++
packages/common-schemas/src/events.ts | 18 ++
pnpm-lock.yaml | 162 ------------------
15 files changed, 478 insertions(+), 213 deletions(-)
create mode 100644 .changeset/cuddly-ligers-reflect.md
create mode 100644 apps/docs/reference/send-event.mdx
create mode 100644 apps/webapp/app/services/events/ingestCustomEvent.server.ts
create mode 100644 apps/webapp/app/utils/json.ts
create mode 100644 apps/webapp/app/utils/objects.ts
diff --git a/.changeset/cuddly-ligers-reflect.md b/.changeset/cuddly-ligers-reflect.md
new file mode 100644
index 00000000000..5b45f5ba958
--- /dev/null
+++ b/.changeset/cuddly-ligers-reflect.md
@@ -0,0 +1,5 @@
+---
+"@trigger.dev/sdk": patch
+---
+
+Added support for delaying delivery when sending custom events
diff --git a/apps/docs/functions/send-event.mdx b/apps/docs/functions/send-event.mdx
index bb299ac2151..9c11909a576 100644
--- a/apps/docs/functions/send-event.mdx
+++ b/apps/docs/functions/send-event.mdx
@@ -31,7 +31,7 @@ curl --request POST \
--header 'Authorization: Bearer ' \
--header 'Content-Type: application/json' \
--data '{
- "id": "",
+ "id": "",
"event": {
"name": "user.created",
"payload": {
@@ -48,12 +48,154 @@ If you are calling this from inside a workflow, ensure that the first parameter
## Sending events from other workflows
-It is useful to send events from workflows because you can split your logic into smaller chunks and reuse them.
+It is useful to send events from workflows because you can split your logic into smaller chunks and reuse them. The below example shows how a scheduled workflow delegates the work via sending a custom event:
-## Writing a workflow that is triggered by an event
+
-You should read [the documentation on custom events](/triggers/custom-events) to learn how to write a workflow that is triggered by an event.
+```ts check-scheduler.ts
+new Trigger({
+ id: "check-scheduler",
+ name: "Check Scheduler",
+ on: scheduleEvent({ rateOf: { minutes: 10 } }),
+ run: async (event, context) => {
+ await context.sendEvent("health.check trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://trigger.dev",
+ host: "trigger.dev",
+ },
+ });
+ await context.sendEvent("health.check docs.trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://docs.trigger.dev",
+ host: "docs.trigger.dev",
+ },
+ });
+
+ await context.sendEvent("health.check app.trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://app.trigger.dev/healthcheck",
+ host: "app.trigger.dev",
+ },
+ });
+ },
+}).listen();
+```
+
+```ts health-check.ts
+export const healthCheck = new Trigger({
+ id: "health-check",
+ name: "Health Check",
+ on: customEvent({
+ name: "health.check",
+ schema: z.object({
+ url: z.string().url(),
+ host: z.string(),
+ }),
+ }),
+ run: async (event, context) => {
+ const response = await context.fetch("fetch site", event.url, {
+ method: "GET",
+ retry: {
+ enabled: false,
+ },
+ });
+
+ if (response.ok) {
+ await context.logger.info(`${event.host} is up!`);
+ return;
+ }
+
+ await slack.postMessage("Site is down", {
+ channelName: "health-checks",
+ text: `${event.host} is down: ${response.status}`,
+ });
+ },
+}).listen();
+```
+
+
+
+## Delaying event delivery
+
+Through both our Node.js SDK and our HTTP API, you can delay the delivery of an event. This is useful if you want to send an event to trigger a workflow, but you want to wait for a certain amount of time before the event is delivered.
+
+Using the health check example above, we can delay the delivery of the `health.check` event in various ways:
+
+```ts
+new Trigger({
+ id: "check-scheduler",
+ name: "Check Scheduler",
+ on: scheduleEvent({ rateOf: { minutes: 10 } }),
+ run: async (event, context) => {
+ await context.sendEvent("health.check trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://trigger.dev",
+ host: "trigger.dev",
+ },
+ delay: { until: new Date(Date.now() + 1000 * 60 * 5) }, // Delay until a specific date
+ });
+
+ await context.sendEvent("health.check docs.trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://docs.trigger.dev",
+ host: "docs.trigger.dev",
+ },
+ delay: { minutes: 5 }, // Delay for a specific amount of time
+ });
+
+ await context.sendEvent("health.check app.trigger.dev", {
+ name: "health.check",
+ payload: {
+ url: "https://app.trigger.dev/healthcheck",
+ host: "app.trigger.dev",
+ },
+ delay: { seconds: 30 }, // Delay for a specific amount of time
+ });
+ },
+}).listen();
```
+
+ When sending events in the context of a workflow run, adding a delay DOES NOT
+ add a delay to the workflow run, as sending a custom event is a
+ fire-and-forget action.
+
+
+You can add delays via the HTTP API by adding a `delay` object to the event:
+
+```bash
+curl --request POST \
+ --url https://app.trigger.dev/api/v1/events \
+ --header 'Authorization: Bearer ' \
+ --header 'Content-Type: application/json' \
+ --data '{
+ "id": "",
+ "event": {
+ "name": "user.created",
+ "payload": {
+ "userId": "123456"
+ },
+ "delay": {
+ "seconds": 30
+ }
+ }
+ }'
+```
+
+## Event deduplication
+
+When sending events, you can specify an `id` for the event. If you send an event with the same `id` multiple times, only the first event will be delivered. This is useful if you want to ensure that an event is only delivered once, even if you send it multiple times.
+
+```ts
+sendEvent({
+ id: "my-event-id",
+ name: "my-event",
+ payload: { foo: "bar" },
+});
```
diff --git a/apps/docs/mint.json b/apps/docs/mint.json
index 41e403a370c..9fa5c4b2284 100644
--- a/apps/docs/mint.json
+++ b/apps/docs/mint.json
@@ -111,7 +111,8 @@
"reference/trigger",
"reference/custom-event",
"reference/webhook-event",
- "reference/schedule-event"
+ "reference/schedule-event",
+ "reference/send-event"
]
},
{
diff --git a/apps/docs/reference/send-event.mdx b/apps/docs/reference/send-event.mdx
new file mode 100644
index 00000000000..06c931d9f0f
--- /dev/null
+++ b/apps/docs/reference/send-event.mdx
@@ -0,0 +1,97 @@
+---
+title: "sendEvent"
+sidebarTitle: "sendEvent"
+description: "Send a custom event to Trigger, from inside or outside of a workflow."
+---
+
+## Usage
+
+```ts
+import { Trigger, customEvent } from "@trigger.dev/sdk";
+
+new Trigger({
+ id: "usage",
+ name: "usage",
+ on: customEvent({
+ name: "user.created",
+ schema: z.any(),
+ }),
+ run: async (event, ctx) => {
+ await ctx.sendEvent("Sending context user.created event", {
+ name: "user.created",
+ payload: {
+ id: "1234_abc",
+ },
+ });
+ },
+}).listen();
+```
+
+You can also use `sendEvent` by importing it from `@trigger.dev/sdk`:
+
+```ts
+import { sendEvent } from "@trigger.dev/sdk";
+
+await sendEvent("Sending context user.created event", {
+ name: "user.created",
+ payload: {
+ id: "1234_abc",
+ },
+});
+```
+
+
+ If calling `sendEvent` from outside of a workflow run, it will make an HTTP
+ API request to app.trigger.dev to send the event. Make sure you set the
+ `TRIGGER_API_KEY` environment variable if you will be using it this way.
+
+
+## Parameters
+
+
+ A unique key for the event in the context of a workflow run. Please see the
+ [Keys and Resumability](/guides/resumability) guide for more info.
+
+
+
+
+
+ An optional unique ID for the event. If not provided, one will be
+ generated automatically (using `clid`). Set this field to perform event
+ deduplication.
+
+
+
+ The name of the event. This is the name you set when creating the
+ `customEvent` trigger.
+
+
+
+ The payload of the event.
+
+
+
+ An optional timestamp for the event. If not provided, one will be generated. Must be in ISO 8601 format (e.g. `new Date().toISOString()`)
+
+
+
+ An optional context object for the event. This can be used to pass
+ additional information about the event, such as the user who triggered
+ it. Note that this is not currently exposed to the workflow (coming soon).
+
+
+
+ An optional delay object for the event. This can be used to delay the
+ event by a certain amount of time. Use one of the following options:
+
+ - `{ seconds: number }` - delay the event by a certain number of seconds
+ - `{ minutes: number }` - delay the event by a certain number of minutes
+ - `{ hours: number }` - delay the event by a certain number of hours
+ - `{ days: number }` - delay the event by a certain number of days
+ - `{ until: Date }` - delay the event until a certain date
+
+ See the [Delaying Event Delivery](/functions/send-event#delaying-event-delivery) docs for more info.
+
+
+
+
diff --git a/apps/webapp/app/models/workflowRun.server.ts b/apps/webapp/app/models/workflowRun.server.ts
index 77c5d976b68..123c6c9fb7a 100644
--- a/apps/webapp/app/models/workflowRun.server.ts
+++ b/apps/webapp/app/models/workflowRun.server.ts
@@ -1,14 +1,16 @@
-import type { WorkflowRun, WorkflowRunStep } from ".prisma/client";
-import type { WorkflowRunStatus } from ".prisma/client";
+import type {
+ WorkflowRun,
+ WorkflowRunStatus,
+ WorkflowRunStep,
+} from ".prisma/client";
import type {
CustomEventSchema,
ErrorSchema,
LogMessageSchema,
} from "@trigger.dev/common-schemas";
-import { ulid } from "ulid";
import type { z } from "zod";
import { prisma } from "~/db.server";
-import { IngestEvent } from "~/services/events/ingest.server";
+import { IngestCustomEvent } from "~/services/events/ingestCustomEvent.server";
import { createStepOnce } from "./workflowRunStep.server";
export type { WorkflowRun, WorkflowRunStep, WorkflowRunStatus };
@@ -160,20 +162,12 @@ export async function triggerEventInRun(
return;
}
- const ingestService = new IngestEvent();
-
- await ingestService.call(
- {
- id: ulid(),
- name: event.name,
- type: "CUSTOM_EVENT",
- service: "trigger",
- payload: event.payload,
- context: event.context,
- apiKey: workflowRun.environment.apiKey,
- },
- workflowRun.environment.organization
- );
+ const ingestService = new IngestCustomEvent();
+
+ await ingestService.call({
+ apiKey: workflowRun.environment.apiKey,
+ event,
+ });
await prisma.workflowRunStep.update({
where: { id: step.step.id },
diff --git a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx
index 6e144aff881..65b3c33ab9d 100644
--- a/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx
+++ b/apps/webapp/app/routes/__app/orgs/$organizationSlug/workflows/$workflowSlug/runs/$runId.tsx
@@ -9,10 +9,10 @@ import {
} from "@heroicons/react/24/outline";
import {
ArrowPathRoundedSquareIcon,
+ ChatBubbleOvalLeftEllipsisIcon,
CheckCircleIcon,
ExclamationCircleIcon,
ExclamationTriangleIcon,
- ChatBubbleOvalLeftEllipsisIcon,
} from "@heroicons/react/24/solid";
import { useFetcher } from "@remix-run/react";
import type { LoaderArgs } from "@remix-run/server-runtime";
@@ -623,6 +623,40 @@ function CustomEventStep({ event }: { event: StepType }) {
{event.input.name}
+ {"delay" in event.input && event.input.delay && (
+ <>
+
+ Delay
+
+
+ {"seconds" in event.input.delay ? (
+ <>
+ {event.input.delay.seconds}{" "}
+ {event.input.delay.seconds > 1 ? "seconds" : "second"}
+ >
+ ) : "minutes" in event.input.delay ? (
+ <>
+ {event.input.delay.minutes}{" "}
+ {event.input.delay.minutes > 1 ? "minutes" : "minute"}
+ >
+ ) : "hours" in event.input.delay ? (
+ <>
+ {event.input.delay.hours}{" "}
+ {event.input.delay.hours > 1 ? "hours" : "hour"}
+ >
+ ) : "days" in event.input.delay ? (
+ <>
+ {event.input.delay.days}{" "}
+ {event.input.delay.days > 1 ? "days" : "day"}
+ >
+ ) : "until" in event.input.delay ? (
+ <>Until {event.input.delay.until}>
+ ) : (
+ <>>
+ )}
+
+ >
+ )}
Payload
{event.input.context && (
diff --git a/apps/webapp/app/routes/api/v1/events.ts b/apps/webapp/app/routes/api/v1/events.ts
index 062d6aaf949..7f1ac0c42ce 100644
--- a/apps/webapp/app/routes/api/v1/events.ts
+++ b/apps/webapp/app/routes/api/v1/events.ts
@@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime";
import { CustomEventSchema } from "@trigger.dev/common-schemas";
import { z } from "zod";
import { authenticateApiRequest } from "~/services/apiAuth.server";
-import { IngestEvent } from "~/services/events/ingest.server";
+import { IngestCustomEvent } from "~/services/events/ingestCustomEvent.server";
const EventBodySchema = z.object({
id: z.string(),
@@ -32,20 +32,13 @@ export async function action({ request }: ActionArgs) {
return json({ error: eventBody.error.message }, { status: 400 });
}
- const service = new IngestEvent();
-
- const result = await service.call(
- {
- id: eventBody.data.id,
- name: eventBody.data.event.name,
- type: "CUSTOM_EVENT",
- service: "trigger",
- payload: eventBody.data.event.payload,
- context: eventBody.data.event.context,
- apiKey: authenticatedEnv.apiKey,
- },
- authenticatedEnv.organization
- );
-
- return json(result.data);
+ const service = new IngestCustomEvent();
+
+ await service.call({
+ id: eventBody.data.id,
+ event: eventBody.data.event,
+ apiKey: authenticatedEnv.apiKey,
+ });
+
+ return { status: 200 };
}
diff --git a/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts b/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts
index 30cd55775e0..9ddc914c4ef 100644
--- a/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts
+++ b/apps/webapp/app/routes/resources/run/$organizationSlug/test/$workflowSlug.ts
@@ -13,6 +13,7 @@ import {
import { getWorkflowFromSlugs } from "~/models/workflow.server";
import { CreateWorkflowTestRun } from "~/services/runs/createTestRun.server";
import { requireUserId } from "~/services/session.server";
+import { safeJsonParse } from "~/utils/json";
const requestSchema = z.object({
eventName: z.string(),
@@ -39,7 +40,15 @@ export const action = async ({ request, params }: ActionArgs) => {
const body = Object.fromEntries(formData.entries());
const { eventName, payload, source } = requestSchema.parse(body);
- const jsonPayload = JSON.parse(payload);
+ const jsonPayload = safeJsonParse(payload);
+
+ if (!jsonPayload) {
+ return redirectWithErrorMessage(
+ redirectUriForSource(source, organizationSlug, workflowSlug),
+ request,
+ "Invalid JSON payload"
+ );
+ }
const workflow = await getWorkflowFromSlugs({
userId,
@@ -98,6 +107,19 @@ export const action = async ({ request, params }: ActionArgs) => {
}
};
+function redirectUriForSource(
+ source: "rerun" | "test",
+ organizationSlug: string,
+ workflowSlug: string,
+ runId?: string
+) {
+ if (source === "rerun") {
+ return `/orgs/${organizationSlug}/workflows/${workflowSlug}/runs/${runId}`;
+ } else {
+ return `/orgs/${organizationSlug}/workflows/${workflowSlug}/test`;
+ }
+}
+
function errorMessageForSource(source: "rerun" | "test") {
if (source === "rerun") {
return "Unable to rerun this workflow. Please contact help@trigger.dev for assistance.";
diff --git a/apps/webapp/app/services/events/ingestCustomEvent.server.ts b/apps/webapp/app/services/events/ingestCustomEvent.server.ts
new file mode 100644
index 00000000000..06673e7762c
--- /dev/null
+++ b/apps/webapp/app/services/events/ingestCustomEvent.server.ts
@@ -0,0 +1,60 @@
+import type { CustomEventSchema } from "@trigger.dev/common-schemas";
+import type { PublishOptions } from "internal-platform";
+import { ulid } from "ulid";
+import type { z } from "zod";
+import { taskQueue } from "~/services/messageBroker.server";
+import { omit } from "~/utils/objects";
+import { IngestEvent } from "./ingest.server";
+
+export type IngestCustomEventOptions = {
+ id?: string;
+ apiKey: string;
+ event: z.infer;
+ isTest?: boolean;
+};
+
+export class IngestCustomEvent {
+ public async call(options: IngestCustomEventOptions) {
+ if (options.event.delay) {
+ const deliveryOptions: PublishOptions =
+ "until" in options.event.delay
+ ? { deliverAt: new Date(options.event.delay.until).getTime() }
+ : "seconds" in options.event.delay
+ ? { deliverAfter: options.event.delay.seconds * 1000 }
+ : "minutes" in options.event.delay
+ ? { deliverAfter: options.event.delay.minutes * 60 * 1000 }
+ : "hours" in options.event.delay
+ ? { deliverAfter: options.event.delay.hours * 60 * 60 * 1000 }
+ : "days" in options.event.delay
+ ? { deliverAfter: options.event.delay.days * 60 * 60 * 24 * 1000 }
+ : { deliverAfter: 0 };
+
+ await taskQueue.publish(
+ "INGEST_DELAYED_EVENT",
+ {
+ id: options.id,
+ apiKey: options.apiKey,
+ event: omit(options.event, ["delay"]),
+ },
+ {},
+ deliveryOptions
+ );
+
+ return;
+ }
+
+ const ingestService = new IngestEvent();
+
+ await ingestService.call({
+ id: options.id ?? ulid(),
+ name: options.event.name,
+ type: "CUSTOM_EVENT",
+ service: "trigger",
+ payload: options.event.payload,
+ context: options.event.context,
+ timestamp: options.event.timestamp,
+ apiKey: options.apiKey,
+ isTest: options.isTest,
+ });
+ }
+}
diff --git a/apps/webapp/app/services/messageBroker.server.ts b/apps/webapp/app/services/messageBroker.server.ts
index ddff46a03e6..1d4f04cc161 100644
--- a/apps/webapp/app/services/messageBroker.server.ts
+++ b/apps/webapp/app/services/messageBroker.server.ts
@@ -35,7 +35,7 @@ import { InitiateDelay } from "./delays/initiateDelay.server";
import { ResolveDelay } from "./delays/resolveDelay.server";
import { sendEmail } from "./email.server";
import { DispatchEvent } from "./events/dispatch.server";
-import { IngestEvent } from "./events/ingest.server";
+import { IngestCustomEvent } from "./events/ingestCustomEvent.server";
import { HandleNewServiceConnection } from "./externalServices/handleNewConnection.server";
import { RegisterExternalSource } from "./externalSources/registerExternalSource.server";
import { CreateFetchRequest } from "./fetches/createFetchRequest.server";
@@ -51,6 +51,7 @@ import { WorkflowRunDisconnected } from "./runs/runDisconnected.server";
import { WorkflowRunTriggerTimeout } from "./runs/runTriggerTimeout.server";
import { DeliverScheduledEvent } from "./scheduler/deliverScheduledEvent.server";
import { RegisterSchedulerSource } from "./scheduler/registerSchedulerSource.server";
+import { omit } from "~/utils/objects";
let pulsarClient: PulsarClient;
let triggerPublisher: ZodPublisher;
@@ -374,6 +375,14 @@ const taskQueueCatalog = {
data: z.object({ id: z.string() }),
properties: z.object({}),
},
+ INGEST_DELAYED_EVENT: {
+ data: z.object({
+ id: z.string().optional(),
+ apiKey: z.string(),
+ event: CustomEventSchema.omit({ delay: true }),
+ }),
+ properties: z.object({}),
+ },
TRIGGER_WORKFLOW_RUN: {
data: z.object({ id: z.string() }),
properties: z.object({}),
@@ -646,6 +655,17 @@ function createTaskQueue() {
return true;
},
+ INGEST_DELAYED_EVENT: async (id, data, properties, attributes) => {
+ if (attributes.redeliveryCount >= 4) {
+ return true;
+ }
+
+ const ingestService = new IngestCustomEvent();
+
+ await ingestService.call(data);
+
+ return true;
+ },
TRIGGER_WORKFLOW_RUN: async (id, data, properties) => {
const run = await findWorklowRunById(data.id);
@@ -690,19 +710,19 @@ function createTaskQueue() {
return true;
}
- const service = new IngestEvent();
+ if (!env.INTERNAL_TRIGGER_API_KEY) {
+ return true;
+ }
- const result = await service.call({
+ const service = new IngestCustomEvent();
+
+ await service.call({
id: data.id,
- name: data.name,
- type: "CUSTOM_EVENT",
- service: "trigger",
- payload: data.payload,
- context: data.context,
+ event: omit(data, ["id"]),
apiKey: env.INTERNAL_TRIGGER_API_KEY,
});
- return result.status === "success";
+ return true;
},
},
});
diff --git a/apps/webapp/app/utils/json.ts b/apps/webapp/app/utils/json.ts
new file mode 100644
index 00000000000..2da50949c96
--- /dev/null
+++ b/apps/webapp/app/utils/json.ts
@@ -0,0 +1,7 @@
+export function safeJsonParse(json: string): unknown {
+ try {
+ return JSON.parse(json);
+ } catch (e) {
+ return null;
+ }
+}
diff --git a/apps/webapp/app/utils/objects.ts b/apps/webapp/app/utils/objects.ts
new file mode 100644
index 00000000000..337fba9cac6
--- /dev/null
+++ b/apps/webapp/app/utils/objects.ts
@@ -0,0 +1,14 @@
+export function omit, K extends keyof T>(
+ obj: T,
+ keys: K[]
+): Omit {
+ const result: any = {};
+
+ for (const key of Object.keys(obj)) {
+ if (!keys.includes(key as K)) {
+ result[key] = obj[key];
+ }
+ }
+
+ return result;
+}
diff --git a/examples/smoke-test/src/index.ts b/examples/smoke-test/src/index.ts
index 62093a8b1cf..c082e487164 100644
--- a/examples/smoke-test/src/index.ts
+++ b/examples/smoke-test/src/index.ts
@@ -23,11 +23,13 @@ const trigger = new Trigger({
await ctx.sendEvent("start-fire", {
name: "smoke.test",
payload: { baz: "banana" },
+ delay: { until: new Date(Date.now() + 1000 * 60) },
});
await sendEvent("start-fire-2", {
name: "smoke.test2",
payload: { baz: "banana2" },
+ delay: { minutes: 1 },
});
return { foo: "bar" };
@@ -36,6 +38,24 @@ const trigger = new Trigger({
trigger.listen();
+new Trigger({
+ id: "smoke-test",
+ name: "Smoke Test",
+ apiKey: "trigger_dev_zC25mKNn6c0q",
+ endpoint: "ws://localhost:8889/ws",
+ logLevel: "debug",
+ on: customEvent({
+ name: "smoke.test",
+ schema: z.object({ baz: z.string() }),
+ }),
+ run: async (event, ctx) => {
+ await ctx.logger.info("Inside the smoke test workflow, received event", {
+ event,
+ myDate: new Date(),
+ });
+ },
+}).listen();
+
new Trigger({
id: "log-tests",
name: "My logs",
diff --git a/packages/common-schemas/src/events.ts b/packages/common-schemas/src/events.ts
index 5ece387271e..37fff268ec6 100644
--- a/packages/common-schemas/src/events.ts
+++ b/packages/common-schemas/src/events.ts
@@ -6,6 +6,15 @@ export const CustomEventSchema = z.object({
payload: JsonSchema,
context: JsonSchema.optional(),
timestamp: z.string().datetime().optional(),
+ delay: z
+ .union([
+ z.object({ seconds: z.number().int() }),
+ z.object({ minutes: z.number().int() }),
+ z.object({ hours: z.number().int() }),
+ z.object({ days: z.number().int() }),
+ z.object({ until: z.string().datetime() }),
+ ])
+ .optional(),
});
export const SerializableCustomEventSchema = z.object({
@@ -13,6 +22,15 @@ export const SerializableCustomEventSchema = z.object({
payload: SerializableJsonSchema,
context: SerializableJsonSchema.optional(),
timestamp: z.string().datetime().optional(),
+ delay: z
+ .union([
+ z.object({ seconds: z.number().int() }),
+ z.object({ minutes: z.number().int() }),
+ z.object({ hours: z.number().int() }),
+ z.object({ days: z.number().int() }),
+ z.object({ until: z.date() }),
+ ])
+ .optional(),
});
const EventMatcherSchema = z.union([
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d061b2ea1b0..b5ab3ac8ccd 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -35,33 +35,6 @@ importers:
devDependencies:
mintlify: 2.0.16
- apps/internal-triggers:
- specifiers:
- '@trigger.dev/integrations': ^0.1.13
- '@trigger.dev/sdk': ^0.2.10
- '@trigger.dev/tsconfig': workspace:*
- '@types/node': '16'
- '@types/pg': ^8.6.6
- dotenv: ^16.0.3
- pg: ^8.8.0
- rimraf: ^3.0.2
- tsup: ^6.5.0
- typescript: ^4.9.4
- zod: ^3.20.2
- dependencies:
- '@trigger.dev/integrations': 0.1.13
- '@trigger.dev/sdk': 0.2.10
- pg: 8.8.0
- zod: 3.20.2
- devDependencies:
- '@trigger.dev/tsconfig': link:../../config-packages/tsconfig
- '@types/node': 16.18.11
- '@types/pg': 8.6.6
- dotenv: 16.0.3
- rimraf: 3.0.2
- tsup: 6.5.0_typescript@4.9.4
- typescript: 4.9.4
-
apps/webapp:
specifiers:
'@aws-sdk/client-s3': ^3.186.0
@@ -5412,48 +5385,6 @@ packages:
engines: {node: '>= 6'}
dev: true
- /@trigger.dev/integrations/0.1.13:
- resolution: {integrity: sha512-2CfQNGqdC82om8Zr+PAdZZWov5SPAAAuVGV39d/MTNFLHhJZmJx47nbc5nnT/ujPSkoRrvOolwl93rVeceYoYA==}
- dependencies:
- '@react-email/render': 0.0.3
- '@trigger.dev/providers': 0.1.7
- '@trigger.dev/sdk': 0.2.10
- zod: 3.20.2
- transitivePeerDependencies:
- - bufferutil
- - encoding
- - react
- - supports-color
- - utf-8-validate
- dev: false
-
- /@trigger.dev/providers/0.1.7:
- resolution: {integrity: sha512-wy7bh/bOGNgYbpUOJz59BkFU/0e3U0p7/5rClYY52QgAXZ9Z6c4JfocCqmYRxh+/Lb18N/wUd+vo5V9YfoyeIQ==}
- dependencies:
- '@shopify/admin-graphql-api-utilities': 2.0.1
- tiny-invariant: 1.3.1
- zod: 3.20.2
- dev: false
-
- /@trigger.dev/sdk/0.2.10:
- resolution: {integrity: sha512-768mZmXi2iLgUfRIKOlpU3q9wRF6anhSnKoToJnRRzyAX3o6Kw5GkBXyM2QVuB5rt6psgbD+M4RKDaTnLpn/wQ==}
- dependencies:
- debug: 4.3.4
- evt: 2.4.13
- node-fetch: 2.6.7
- slug: 6.1.0
- ulid: 2.3.0
- uuid: 9.0.0
- ws: 8.12.0
- zod: 3.20.2
- zod-error: 1.1.0
- transitivePeerDependencies:
- - bufferutil
- - encoding
- - supports-color
- - utf-8-validate
- dev: false
-
/@tsconfig/node10/1.0.9:
resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==}
@@ -5724,14 +5655,6 @@ packages:
/@types/normalize-package-data/2.4.1:
resolution: {integrity: sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==}
- /@types/pg/8.6.6:
- resolution: {integrity: sha512-O2xNmXebtwVekJDD+02udOncjVcMZQuTEQEMpKJ0ZRf5E7/9JJX3izhKUcUifBkyKpljyUM6BTgy2trmviKlpw==}
- dependencies:
- '@types/node': 18.11.18
- pg-protocol: 1.5.0
- pg-types: 2.2.0
- dev: true
-
/@types/prismjs/1.26.0:
resolution: {integrity: sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==}
dev: true
@@ -6837,11 +6760,6 @@ packages:
resolution: {integrity: sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==}
engines: {node: '>=0.10'}
- /buffer-writer/2.0.0:
- resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==}
- engines: {node: '>=4'}
- dev: false
-
/buffer/5.7.1:
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
dependencies:
@@ -13195,10 +13113,6 @@ packages:
semver: 6.3.0
dev: true
- /packet-reader/1.0.0:
- resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==}
- dev: false
-
/pako/0.2.9:
resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==}
dev: true
@@ -13357,59 +13271,6 @@ packages:
is-reference: 3.0.1
dev: true
- /pg-connection-string/2.5.0:
- resolution: {integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==}
- dev: false
-
- /pg-int8/1.0.1:
- resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
- engines: {node: '>=4.0.0'}
-
- /pg-pool/3.5.2_pg@8.8.0:
- resolution: {integrity: sha512-His3Fh17Z4eg7oANLob6ZvH8xIVen3phEZh2QuyrIl4dQSDVEabNducv6ysROKpDNPSD+12tONZVWfSgMvDD9w==}
- peerDependencies:
- pg: '>=8.0'
- dependencies:
- pg: 8.8.0
- dev: false
-
- /pg-protocol/1.5.0:
- resolution: {integrity: sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==}
-
- /pg-types/2.2.0:
- resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
- engines: {node: '>=4'}
- dependencies:
- pg-int8: 1.0.1
- postgres-array: 2.0.0
- postgres-bytea: 1.0.0
- postgres-date: 1.0.7
- postgres-interval: 1.2.0
-
- /pg/8.8.0:
- resolution: {integrity: sha512-UXYN0ziKj+AeNNP7VDMwrehpACThH7LUl/p8TDFpEUuSejCUIwGSfxpHsPvtM6/WXFy6SU4E5RG4IJV/TZAGjw==}
- engines: {node: '>= 8.0.0'}
- peerDependencies:
- pg-native: '>=3.0.1'
- peerDependenciesMeta:
- pg-native:
- optional: true
- dependencies:
- buffer-writer: 2.0.0
- packet-reader: 1.0.0
- pg-connection-string: 2.5.0
- pg-pool: 3.5.2_pg@8.8.0
- pg-protocol: 1.5.0
- pg-types: 2.2.0
- pgpass: 1.0.5
- dev: false
-
- /pgpass/1.0.5:
- resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
- dependencies:
- split2: 4.1.0
- dev: false
-
/picocolors/1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
@@ -13563,24 +13424,6 @@ packages:
picocolors: 1.0.0
source-map-js: 1.0.2
- /postgres-array/2.0.0:
- resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
- engines: {node: '>=4'}
-
- /postgres-bytea/1.0.0:
- resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
- engines: {node: '>=0.10.0'}
-
- /postgres-date/1.0.7:
- resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
- engines: {node: '>=0.10.0'}
-
- /postgres-interval/1.2.0:
- resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
- engines: {node: '>=0.10.0'}
- dependencies:
- xtend: 4.0.2
-
/posthog-js/1.39.4:
resolution: {integrity: sha512-Elpf1gwyuObueXi89iH+9pP+WhpkiivP8Qwej4RzOLwSTa7Floaa4rgAw7rnCnX1PtRoJ3F0kqb6q9T+aZjRiA==}
dependencies:
@@ -15112,11 +14955,6 @@ packages:
through: 2.3.8
dev: true
- /split2/4.1.0:
- resolution: {integrity: sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ==}
- engines: {node: '>= 10.x'}
- dev: false
-
/sprintf-js/1.0.3:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
From 057a13b52c7e93f52456584e2bd492a5fc1f4269 Mon Sep 17 00:00:00 2001
From: Eric Allam
Date: Fri, 27 Jan 2023 15:55:55 +0000
Subject: [PATCH 21/39] Slack integration improvements
- Add ability to trigger workflows on block actions
- Add reactions
- Add ability to respond to a block action interaction
---
.../app/components/triggers/triggerLabel.tsx | 2 +
.../models/workflowListPresenter.server.ts | 56 ++++-
.../internal/webhooks/slack/interactivity.ts | 19 ++
.../app/services/events/dispatch.server.ts | 20 +-
.../performIntegrationRequest.server.ts | 3 +
.../slack/handleInteractivity.server.ts | 73 ++++++
.../workflows/registerWorkflow.server.ts | 40 +++-
.../migration.sql | 2 +
.../migration.sql | 33 +++
apps/webapp/prisma/schema.prisma | 39 +++
examples/send-to-slack/src/index.ts | 135 ++++++++++-
packages/common-schemas/src/events.ts | 9 +
packages/common-schemas/src/triggers.ts | 18 +-
.../src/slack/requests.ts | 225 +++++++++++++++++-
packages/internal-integrations/src/types.ts | 1 +
.../src/integrations/slack/events.ts | 37 +++
.../src/integrations/slack/index.ts | 64 +++++
.../src/providers/slack/index.ts | 1 +
.../src/providers/slack/interactivity.ts | 76 +-----
.../src/providers/slack/prodmanifest.json | 6 +-
.../src/providers/slack/schemas.ts | 37 +++
21 files changed, 810 insertions(+), 86 deletions(-)
create mode 100644 apps/webapp/app/services/slack/handleInteractivity.server.ts
create mode 100644 apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql
create mode 100644 apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql
create mode 100644 packages/trigger-integrations/src/integrations/slack/events.ts
diff --git a/apps/webapp/app/components/triggers/triggerLabel.tsx b/apps/webapp/app/components/triggers/triggerLabel.tsx
index 07f1bca99e3..5558fcb046d 100644
--- a/apps/webapp/app/components/triggers/triggerLabel.tsx
+++ b/apps/webapp/app/components/triggers/triggerLabel.tsx
@@ -12,6 +12,8 @@ export function triggerLabel(type: TriggerType) {
return "HTTP endpoint";
case "SCHEDULE":
return "Scheduled";
+ case "SLACK_INTERACTION":
+ return "Slack interaction";
default:
return type;
}
diff --git a/apps/webapp/app/models/workflowListPresenter.server.ts b/apps/webapp/app/models/workflowListPresenter.server.ts
index 11831225574..956610654b7 100644
--- a/apps/webapp/app/models/workflowListPresenter.server.ts
+++ b/apps/webapp/app/models/workflowListPresenter.server.ts
@@ -1,5 +1,8 @@
-import type { SchedulerSource } from ".prisma/client";
-import { ScheduleSourceSchema } from "@trigger.dev/common-schemas";
+import type { SchedulerSource, InternalSource } from ".prisma/client";
+import {
+ ScheduleSourceSchema,
+ SlackInteractionSourceSchema,
+} from "@trigger.dev/common-schemas";
import cronstrue from "cronstrue";
import type { DisplayProperties } from "internal-integrations";
import { github } from "internal-integrations";
@@ -60,7 +63,8 @@ export class WorkflowListPresenter {
trigger: triggerProperties(
workflow,
workflow.externalSource ?? undefined,
- workflow.schedulerSources[0] ?? undefined
+ workflow.schedulerSources[0] ?? undefined,
+ workflow.internalSources[0] ?? undefined
),
integrations: {
source: workflow.service
@@ -105,6 +109,17 @@ function getWorkflows(
orderBy: { createdAt: "desc" },
take: 1,
},
+ internalSources: {
+ select: {
+ source: true,
+ type: true,
+ },
+ where: {
+ environmentId,
+ },
+ orderBy: { createdAt: "desc" },
+ take: 1,
+ },
runs: {
select: {
finishedAt: true,
@@ -124,7 +139,8 @@ function getWorkflows(
function triggerProperties(
workflow: Pick,
externalSource?: Pick,
- schedulerSource?: Pick
+ schedulerSource?: Pick,
+ internalSource?: Pick
): {
type: Workflow["type"];
typeTitle: string;
@@ -157,7 +173,14 @@ function triggerProperties(
};
}
case "SCHEDULE": {
- invariant(schedulerSource, "Scheduler source is required for schedule");
+ if (!schedulerSource) {
+ return {
+ type: workflow.type,
+ typeTitle: "Schedule",
+ title: "Not configured",
+ };
+ }
+
const source = ScheduleSourceSchema.parse(schedulerSource.schedule);
if ("rateOf" in source) {
@@ -205,6 +228,29 @@ function triggerProperties(
typeTitle: "Custom event",
title: `on: ${workflow.eventNames.join(", ")}`,
};
+ case "SLACK_INTERACTION": {
+ if (!internalSource) {
+ return {
+ type: workflow.type,
+ typeTitle: "Slack interaction",
+ title: "on: Slack interaction",
+ };
+ }
+
+ const slackSource = SlackInteractionSourceSchema.parse(
+ internalSource.source
+ );
+
+ return {
+ type: workflow.type,
+ typeTitle: "Slack interaction",
+ title: `block_id = ${slackSource.blockId}`,
+ properties:
+ slackSource.actionIds.length > 0
+ ? [{ key: "Action ID", value: slackSource.actionIds.join(", ") }]
+ : undefined,
+ };
+ }
default: {
return {
type: workflow.type,
diff --git a/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts b/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts
index c9798e7de10..f3c988ef7e2 100644
--- a/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts
+++ b/apps/webapp/app/routes/api/v1/internal/webhooks/slack/interactivity.ts
@@ -1,5 +1,24 @@
import type { ActionArgs } from "@remix-run/server-runtime";
+import { HandleSlackInteractivity } from "~/services/slack/handleInteractivity.server";
export async function action({ request }: ActionArgs) {
+ const formData = await request.formData();
+
+ const payload = formData.get("payload");
+
+ if (typeof payload !== "string") {
+ return { status: 400 };
+ }
+
+ const parsedPayload = JSON.parse(payload);
+
+ const service = new HandleSlackInteractivity();
+
+ try {
+ await service.call(parsedPayload);
+ } catch (error) {
+ console.error(error);
+ }
+
return { status: 200 };
}
diff --git a/apps/webapp/app/services/events/dispatch.server.ts b/apps/webapp/app/services/events/dispatch.server.ts
index 7aa3366a96a..f547d2efca6 100644
--- a/apps/webapp/app/services/events/dispatch.server.ts
+++ b/apps/webapp/app/services/events/dispatch.server.ts
@@ -127,14 +127,22 @@ class EventMatcher {
}
function patternMatches(payload: any, pattern: any): boolean {
- for (const [key, value] of Object.entries(pattern)) {
- if (Array.isArray(value)) {
- if (!value.includes(payload[key])) {
+ for (const [patternKey, patternValue] of Object.entries(pattern)) {
+ const payloadValue = payload[patternKey];
+
+ if (Array.isArray(patternValue)) {
+ if (patternValue.length > 0 && !patternValue.includes(payloadValue)) {
return false;
}
- } else if (typeof value === "object") {
- if (!patternMatches(payload[key], value)) {
- return false;
+ } else if (typeof patternValue === "object") {
+ if (Array.isArray(payloadValue)) {
+ if (!payloadValue.some((item) => patternMatches(item, patternValue))) {
+ return false;
+ }
+ } else {
+ if (!patternMatches(payloadValue, patternValue)) {
+ return false;
+ }
}
}
}
diff --git a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts
index 320042e1fd5..0c2275dfc9b 100644
--- a/apps/webapp/app/services/requests/performIntegrationRequest.server.ts
+++ b/apps/webapp/app/services/requests/performIntegrationRequest.server.ts
@@ -231,6 +231,7 @@ export class PerformIntegrationRequest {
endpoint: integrationRequest.endpoint,
params: integrationRequest.params,
cache,
+ metadata: { requestId: integrationRequest.id },
});
}
case "shopify": {
@@ -239,6 +240,7 @@ export class PerformIntegrationRequest {
endpoint: integrationRequest.endpoint,
params: integrationRequest.params,
cache,
+ metadata: { requestId: integrationRequest.id },
});
}
case "resend": {
@@ -247,6 +249,7 @@ export class PerformIntegrationRequest {
endpoint: integrationRequest.endpoint,
params: integrationRequest.params,
cache,
+ metadata: { requestId: integrationRequest.id },
});
}
default: {
diff --git a/apps/webapp/app/services/slack/handleInteractivity.server.ts b/apps/webapp/app/services/slack/handleInteractivity.server.ts
new file mode 100644
index 00000000000..1d3b55c38a2
--- /dev/null
+++ b/apps/webapp/app/services/slack/handleInteractivity.server.ts
@@ -0,0 +1,73 @@
+import { slack } from "@trigger.dev/providers";
+import { ulid } from "ulid";
+import { generateErrorMessage } from "zod-error";
+import type { PrismaClient } from "~/db.server";
+import { prisma } from "~/db.server";
+import { IngestEvent } from "../events/ingest.server";
+
+export class HandleSlackInteractivity {
+ #prismaClient: PrismaClient;
+
+ constructor(prismaClient: PrismaClient = prisma) {
+ this.#prismaClient = prismaClient;
+ }
+
+ public async call(payload: unknown) {
+ console.log("payload", JSON.stringify(payload, null, 2));
+
+ const parsedPayload = slack.schemas.blockAction.safeParse(payload);
+
+ if (!parsedPayload.success) {
+ console.error(
+ "Invalid payload",
+ generateErrorMessage(parsedPayload.error.issues)
+ );
+
+ return;
+ }
+
+ if (parsedPayload.data.type !== "block_actions") {
+ return;
+ }
+
+ if (!parsedPayload.data.message) {
+ return;
+ }
+
+ if (!parsedPayload.data.message.metadata) {
+ return;
+ }
+
+ const { requestId } = parsedPayload.data.message.metadata.event_payload;
+
+ const integrationRequest =
+ await this.#prismaClient.integrationRequest.findUnique({
+ where: { id: requestId },
+ include: {
+ run: {
+ include: {
+ environment: true,
+ workflow: true,
+ },
+ },
+ },
+ });
+
+ if (!integrationRequest) {
+ return;
+ }
+
+ const ingestService = new IngestEvent();
+
+ await ingestService.call({
+ id: ulid(),
+ type: "SLACK_INTERACTION",
+ name: "block.action",
+ service: "slack",
+ payload: parsedPayload.data,
+ apiKey: integrationRequest.run.environment.apiKey,
+ });
+
+ return true;
+ }
+}
diff --git a/apps/webapp/app/services/workflows/registerWorkflow.server.ts b/apps/webapp/app/services/workflows/registerWorkflow.server.ts
index 750819cec73..90da212f76b 100644
--- a/apps/webapp/app/services/workflows/registerWorkflow.server.ts
+++ b/apps/webapp/app/services/workflows/registerWorkflow.server.ts
@@ -45,12 +45,23 @@ export class RegisterWorkflow {
}
if (validation.data.trigger.service !== "trigger") {
- await this.#upsertExternalSource(
+ const source = await this.upsertSource(
validation.data,
organization,
workflow,
environment
);
+
+ if (source && source.status === "READY") {
+ await this.#prismaClient.workflow.update({
+ where: {
+ id: workflow.id,
+ },
+ data: {
+ status: "READY",
+ },
+ });
+ }
}
await this.#upsertEventRule(
@@ -155,7 +166,7 @@ export class RegisterWorkflow {
return workflow;
}
- async #upsertExternalSource(
+ async upsertSource(
payload: WorkflowMetadata,
organization: Organization,
workflow: Workflow,
@@ -220,6 +231,31 @@ export class RegisterWorkflow {
return schedulerSource;
}
+ case "SLACK_INTERACTION": {
+ if (!payload.trigger.source) {
+ return;
+ }
+
+ return this.#prismaClient.internalSource.upsert({
+ where: {
+ workflowId_environmentId: {
+ workflowId: workflow.id,
+ environmentId: environment.id,
+ },
+ },
+ update: {
+ source: payload.trigger.source,
+ },
+ create: {
+ organizationId: organization.id,
+ workflowId: workflow.id,
+ environmentId: environment.id,
+ source: payload.trigger.source,
+ status: "READY",
+ type: "SLACK",
+ },
+ });
+ }
default: {
return;
}
diff --git a/apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql b/apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql
new file mode 100644
index 00000000000..e9b9eceef04
--- /dev/null
+++ b/apps/webapp/prisma/migrations/20230127121743_add_slack_interaction_trigger_type/migration.sql
@@ -0,0 +1,2 @@
+-- AlterEnum
+ALTER TYPE "TriggerType" ADD VALUE 'SLACK_INTERACTION';
diff --git a/apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql b/apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql
new file mode 100644
index 00000000000..2bbe8c1cba7
--- /dev/null
+++ b/apps/webapp/prisma/migrations/20230127123917_add_internal_source_model/migration.sql
@@ -0,0 +1,33 @@
+-- CreateEnum
+CREATE TYPE "InternalSourceType" AS ENUM ('SLACK');
+
+-- CreateEnum
+CREATE TYPE "InternalSourceStatus" AS ENUM ('CREATED', 'READY', 'CANCELLED');
+
+-- CreateTable
+CREATE TABLE "InternalSource" (
+ "id" TEXT NOT NULL,
+ "organizationId" TEXT NOT NULL,
+ "workflowId" TEXT NOT NULL,
+ "environmentId" TEXT NOT NULL,
+ "type" "InternalSourceType" NOT NULL,
+ "source" JSONB NOT NULL,
+ "status" "InternalSourceStatus" NOT NULL DEFAULT 'CREATED',
+ "readyAt" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "InternalSource_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "InternalSource_workflowId_environmentId_key" ON "InternalSource"("workflowId", "environmentId");
+
+-- AddForeignKey
+ALTER TABLE "InternalSource" ADD CONSTRAINT "InternalSource_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "InternalSource" ADD CONSTRAINT "InternalSource_workflowId_fkey" FOREIGN KEY ("workflowId") REFERENCES "Workflow"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "InternalSource" ADD CONSTRAINT "InternalSource_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "RuntimeEnvironment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/apps/webapp/prisma/schema.prisma b/apps/webapp/prisma/schema.prisma
index a3b0319619e..ca286c50ced 100644
--- a/apps/webapp/prisma/schema.prisma
+++ b/apps/webapp/prisma/schema.prisma
@@ -52,6 +52,7 @@ model Organization {
externalSources ExternalSource[]
eventRules EventRule[]
schedulerSources SchedulerSource[]
+ internalSources InternalSource[]
}
model APIConnection {
@@ -107,6 +108,7 @@ model RuntimeEnvironment {
runs WorkflowRun[]
eventRules EventRule[]
schedulerSources SchedulerSource[]
+ internalSources InternalSource[]
@@unique([organizationId, slug])
}
@@ -134,6 +136,7 @@ model Workflow {
rules EventRule[]
externalServices ExternalService[]
schedulerSources SchedulerSource[]
+ internalSources InternalSource[]
service String @default("trigger")
eventNames String[]
@@ -181,6 +184,7 @@ enum TriggerType {
HTTP_ENDPOINT
EVENT_BRIDGE
HTTP_POLLING
+ SLACK_INTERACTION
}
enum WorkflowStatus {
@@ -257,6 +261,41 @@ enum SchedulerSourceStatus {
CANCELLED
}
+// We should eventually move SchedulerSource to this as it's more generic
+model InternalSource {
+ id String @id @default(cuid())
+
+ organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ organizationId String
+
+ workflow Workflow @relation(fields: [workflowId], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ workflowId String
+
+ environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
+ environmentId String
+
+ type InternalSourceType
+ source Json
+
+ status InternalSourceStatus @default(CREATED)
+
+ readyAt DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@unique([workflowId, environmentId])
+}
+
+enum InternalSourceType {
+ SLACK
+}
+
+enum InternalSourceStatus {
+ CREATED
+ READY
+ CANCELLED
+}
+
model ExternalService {
id String @id @default(cuid())
slug String
diff --git a/examples/send-to-slack/src/index.ts b/examples/send-to-slack/src/index.ts
index 2efec4c8eba..19c70079ac5 100644
--- a/examples/send-to-slack/src/index.ts
+++ b/examples/send-to-slack/src/index.ts
@@ -17,10 +17,6 @@ new Trigger({
}),
}),
run: async (event, ctx) => {
- await ctx.logger.info(
- "Received domain.created event, waiting for 1 minutes..."
- );
-
const response = await slack.postMessage("send-to-slack", {
channelName: "test-integrations",
text: `New domain created: ${event.domain} by customer ${event.customerId} cc @Eric #general`,
@@ -142,3 +138,134 @@ function getRandomQuote() {
return arnoldQuotes[Math.floor(Math.random() * arnoldQuotes.length)];
}
+
+const BLOCK_ID = "issue.action.block";
+
+new Trigger({
+ id: "slack-interactivity",
+ name: "Testing Slack Interactivity",
+ apiKey: "trigger_dev_zC25mKNn6c0q",
+ endpoint: "ws://localhost:8889/ws",
+ logLevel: "debug",
+ on: customEvent({
+ name: "slack.interact",
+ schema: z.any(),
+ }),
+ run: async (event, ctx) => {
+ await slack.postMessage("send-to-slack", {
+ channelName: "test-integrations",
+ text: `Click on one of the buttons:`,
+ blocks: [
+ {
+ type: "actions",
+ block_id: BLOCK_ID,
+ elements: [
+ {
+ type: "button",
+ action_id: "close_issue_123",
+ text: {
+ type: "plain_text",
+ text: "Close Issue",
+ emoji: true,
+ },
+ value: "close_issue",
+ },
+ {
+ type: "button",
+ action_id: "comment_issue_123",
+ text: {
+ type: "plain_text",
+ text: "Comment",
+ emoji: true,
+ },
+ value: "comment_issue",
+ },
+ {
+ type: "button",
+ action_id: "view_issue_123",
+ text: {
+ type: "plain_text",
+ text: "View Issue",
+ emoji: true,
+ },
+ value: "view_issue",
+ url: "https://github.com/triggerdotdev/trigger.dev/issues/11",
+ },
+ ],
+ },
+ ],
+ });
+ },
+}).listen();
+
+const BLOCK_ID_2 = "release.action.block";
+
+new Trigger({
+ id: "slack-block-interaction",
+ name: "Slack Block Interaction",
+ apiKey: "trigger_dev_zC25mKNn6c0q",
+ endpoint: "ws://localhost:8889/ws",
+ logLevel: "debug",
+ on: slack.events.blockActionInteraction({
+ blockId: BLOCK_ID,
+ actionId: ["comment_issue_123", "close_issue_123"],
+ }),
+ run: async (event, ctx) => {
+ await slack.postMessageResponse("Response to user", event.response_url, {
+ text: `Thanks for clicking on the button!`,
+ replace_original: false,
+ });
+
+ await slack.postMessageResponse("Respond to everyone", event.response_url, {
+ text: `Now what do you want to do about the PR review?`,
+ replace_original: false,
+ response_type: "in_channel",
+ blocks: [
+ {
+ type: "actions",
+ block_id: BLOCK_ID_2,
+ elements: [
+ {
+ type: "button",
+ action_id: "submit_review_123",
+ text: {
+ type: "plain_text",
+ text: "Submit Review",
+ emoji: true,
+ },
+ value: "submit_review",
+ },
+ {
+ type: "button",
+ action_id: "close_review_123",
+ text: {
+ type: "plain_text",
+ text: "Close Review",
+ emoji: true,
+ },
+ value: "close_review",
+ },
+ ],
+ },
+ ],
+ });
+ },
+}).listen();
+
+new Trigger({
+ id: "slack-block-interaction-2",
+ name: "Slack Block Interaction 2",
+ apiKey: "trigger_dev_zC25mKNn6c0q",
+ endpoint: "ws://localhost:8889/ws",
+ logLevel: "debug",
+ on: slack.events.blockActionInteraction({
+ blockId: BLOCK_ID_2,
+ }),
+ run: async (event, ctx) => {
+ await slack.addReaction("React to message", {
+ name: "thumbsup",
+ timestamp: event.message.ts,
+ channelId: event.channel.id,
+ });
+ },
+}).listen();
diff --git a/packages/common-schemas/src/events.ts b/packages/common-schemas/src/events.ts
index 37fff268ec6..b64e0d08948 100644
--- a/packages/common-schemas/src/events.ts
+++ b/packages/common-schemas/src/events.ts
@@ -89,3 +89,12 @@ export const ManualWebhookSourceSchema = z.object({
}),
event: z.string(),
});
+
+export const SlackInteractionSourceSchema = z.object({
+ blockId: z.string(),
+ actionIds: z.array(z.string()),
+});
+
+export type SlackInteractionSource = z.infer<
+ typeof SlackInteractionSourceSchema
+>;
diff --git a/packages/common-schemas/src/triggers.ts b/packages/common-schemas/src/triggers.ts
index 539adffa4fe..67fefdafc80 100644
--- a/packages/common-schemas/src/triggers.ts
+++ b/packages/common-schemas/src/triggers.ts
@@ -1,5 +1,9 @@
import { z } from "zod";
-import { EventFilterSchema, ScheduleSourceSchema } from "./events";
+import {
+ EventFilterSchema,
+ ScheduleSourceSchema,
+ SlackInteractionSourceSchema,
+} from "./events";
import { JsonSchema } from "./json";
export const CustomEventTriggerSchema = z.object({
@@ -36,11 +40,23 @@ export const ScheduledEventTriggerSchema = z.object({
});
export type ScheduledEventTrigger = z.infer;
+export const SlackInteractionTriggerSchema = z.object({
+ type: z.literal("SLACK_INTERACTION"),
+ service: z.literal("slack"),
+ name: z.string(),
+ filter: EventFilterSchema,
+ source: SlackInteractionSourceSchema,
+});
+export type SlackInteractionEventTrigger = z.infer<
+ typeof SlackInteractionTriggerSchema
+>;
+
export const TriggerMetadataSchema = z.discriminatedUnion("type", [
CustomEventTriggerSchema,
WebhookEventTriggerSchema,
HttpEventTriggerSchema,
ScheduledEventTriggerSchema,
+ SlackInteractionTriggerSchema,
]);
export type TriggerMetadata = z.infer;
diff --git a/packages/internal-integrations/src/slack/requests.ts b/packages/internal-integrations/src/slack/requests.ts
index e4c9a4b7792..163bd77a6eb 100644
--- a/packages/internal-integrations/src/slack/requests.ts
+++ b/packages/internal-integrations/src/slack/requests.ts
@@ -18,6 +18,9 @@ const log = debug("trigger:integrations:slack");
const SendSlackMessageRequestBodySchema =
slack.schemas.PostMessageBodySchema.extend({
link_names: z.literal(1),
+ metadata: z
+ .object({ event_type: z.string(), event_payload: z.any() })
+ .optional(),
});
class SlackRequestIntegration implements RequestIntegration {
@@ -45,6 +48,15 @@ class SlackRequestIntegration implements RequestIntegration {
path: "/chat.postMessage",
});
+ #addReactionEndpoint = new HttpEndpoint<
+ typeof slack.schemas.AddReactionResponseSchema,
+ typeof slack.schemas.AddReactionOptionsSchema
+ >({
+ response: slack.schemas.AddReactionResponseSchema,
+ method: "POST",
+ path: "/reactions.add",
+ });
+
constructor(private readonly baseUrl: string = "https://slack.com/api") {}
perform(options: PerformRequestOptions): Promise {
@@ -53,7 +65,24 @@ class SlackRequestIntegration implements RequestIntegration {
return this.#postMessage(
options.accessInfo,
options.params,
- options.cache
+ options.cache,
+ options.metadata
+ );
+ }
+ case "chat.postMessageResponse": {
+ return this.#postMessageResponse(
+ options.accessInfo,
+ options.params,
+ options.cache,
+ options.metadata
+ );
+ }
+ case "reactions.add": {
+ return this.#addReaction(
+ options.accessInfo,
+ options.params,
+ options.cache,
+ options.metadata
);
}
default: {
@@ -77,6 +106,24 @@ class SlackRequestIntegration implements RequestIntegration {
],
};
}
+ case "chat.postMessageResponse": {
+ return {
+ title: `Post response`,
+ properties: [],
+ };
+ }
+ case "reactions.add": {
+ return {
+ title: `Add reaction to message ${params.timestamp}`,
+ properties: [
+ {
+ key: "Reaction",
+ value: params.name,
+ },
+ ],
+ };
+ }
+
default: {
throw new Error(`Unknown endpoint: ${endpoint}`);
}
@@ -90,7 +137,8 @@ class SlackRequestIntegration implements RequestIntegration {
async #postMessage(
accessInfo: AccessInfo,
params: any,
- cache?: CacheService
+ cache?: CacheService,
+ metadata?: Record
): Promise {
const parsedParams = slack.schemas.PostMessageOptionsSchema.parse(params);
@@ -127,6 +175,9 @@ class SlackRequestIntegration implements RequestIntegration {
...parsedParams,
link_names: 1,
channel: channelId,
+ metadata: metadata
+ ? { event_type: "post_message", event_payload: metadata }
+ : undefined,
});
if (!response.success) {
@@ -185,6 +236,172 @@ class SlackRequestIntegration implements RequestIntegration {
return performedRequest;
}
+ async #addReaction(
+ accessInfo: AccessInfo,
+ params: any,
+ cache?: CacheService,
+ metadata?: Record
+ ): Promise {
+ const parsedParams = slack.schemas.AddReactionOptionsSchema.parse(params);
+
+ log("reactions.add %O", parsedParams);
+
+ const accessToken = getAccessToken(accessInfo);
+
+ const service = new HttpService({
+ accessToken,
+ baseUrl: this.baseUrl,
+ });
+
+ const channelId = await this.#findChannelId(service, parsedParams, cache);
+
+ if (!channelId) {
+ return {
+ ok: false,
+ isRetryable: false,
+ response: {
+ output: {
+ message: `channelId not found`,
+ },
+ context: {
+ statusCode: 404,
+ headers: {},
+ },
+ },
+ };
+ }
+
+ log("found channelId %s", channelId);
+
+ const response = await service.performRequest(this.#addReactionEndpoint, {
+ ...parsedParams,
+ // @ts-ignore
+ channel: channelId,
+ });
+
+ if (!response.success) {
+ log("reactions.add failed %O", response);
+
+ return {
+ ok: false,
+ isRetryable: this.#isRetryable(response.statusCode),
+ response: {
+ output: response.error,
+ context: {
+ statusCode: response.statusCode,
+ headers: response.headers,
+ },
+ },
+ };
+ }
+
+ if (!response.data.ok && response.data.error === "not_in_channel") {
+ log(
+ "reactions.add failed with not_in_channel, attempting to join channel %s",
+ channelId
+ );
+
+ // Attempt to join the channel, and then retry the request
+ const joinResponse = await service.performRequest(
+ this.#joinChannelEndpoint,
+ {
+ channel: channelId,
+ }
+ );
+
+ if (joinResponse.success && joinResponse.data.ok) {
+ log("joined channel %s, retrying reactions.add", channelId);
+
+ return this.#addReaction(accessInfo, params);
+ }
+ }
+
+ const ok = response.data.ok;
+
+ const performedRequest = {
+ ok,
+ isRetryable: this.#isRetryable(response.statusCode),
+ response: {
+ output: response.data,
+ context: {
+ statusCode: response.statusCode,
+ headers: response.headers,
+ },
+ },
+ };
+
+ log("chat.postMessage performedRequest %O", performedRequest);
+
+ return performedRequest;
+ }
+
+ async #postMessageResponse(
+ accessInfo: AccessInfo,
+ params: any,
+ cache?: CacheService,
+ metadata?: Record
+ ): Promise {
+ const parsedParams = z
+ .object({
+ message: slack.schemas.PostMessageResponseOptionsSchema,
+ responseUrl: z.string(),
+ })
+ .parse(params);
+
+ log("chat.postMessageResponse %O", parsedParams);
+
+ const response = await fetch(parsedParams.responseUrl, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ ...parsedParams.message,
+ metadata: metadata
+ ? { event_type: "post_message_response", event_payload: metadata }
+ : undefined,
+ }),
+ });
+
+ if (!response.ok) {
+ log("chat.postMessageResponse failed %O", response);
+
+ const error = await safeGetJson(response);
+
+ return {
+ ok: false,
+ isRetryable: this.#isRetryable(response.status),
+ response: {
+ output: error
+ ? error
+ : { name: `${response.status}`, message: response.statusText },
+ context: {
+ statusCode: response.status,
+ headers: response.headers,
+ },
+ },
+ };
+ }
+
+ const output = await safeGetJson(response);
+
+ const performedRequest = {
+ ok: response.ok,
+ isRetryable: this.#isRetryable(response.status),
+ response: {
+ output,
+ context: {
+ statusCode: response.status,
+ headers: response.headers,
+ },
+ },
+ };
+
+ log("chat.postMessage performedRequest %O", performedRequest);
+
+ return performedRequest;
+ }
+
#isRetryable(statusCode: number): boolean {
return (
statusCode === 408 ||
@@ -244,3 +461,7 @@ class SlackRequestIntegration implements RequestIntegration {
}
export const requests = new SlackRequestIntegration();
+
+function safeGetJson(response: Response): Promise {
+ return response.json().catch(() => null);
+}
diff --git a/packages/internal-integrations/src/types.ts b/packages/internal-integrations/src/types.ts
index c9006a3159b..68eba80be31 100644
--- a/packages/internal-integrations/src/types.ts
+++ b/packages/internal-integrations/src/types.ts
@@ -42,6 +42,7 @@ export type PerformRequestOptions = {
endpoint: string;
params: any;
cache?: CacheService;
+ metadata?: Record;
};
export type DisplayProperties = {
diff --git a/packages/trigger-integrations/src/integrations/slack/events.ts b/packages/trigger-integrations/src/integrations/slack/events.ts
new file mode 100644
index 00000000000..67dc5afb914
--- /dev/null
+++ b/packages/trigger-integrations/src/integrations/slack/events.ts
@@ -0,0 +1,37 @@
+import { slack } from "@trigger.dev/providers";
+import { TriggerEvent } from "@trigger.dev/sdk";
+
+export function blockActionInteraction(params: {
+ blockId: string;
+ actionId?: string | string[];
+}): TriggerEvent {
+ const actionIds =
+ typeof params.actionId === "undefined"
+ ? []
+ : Array.isArray(params.actionId)
+ ? params.actionId
+ : [params.actionId];
+
+ return {
+ metadata: {
+ type: "SLACK_INTERACTION",
+ service: "slack",
+ name: "block.action",
+ filter: {
+ service: ["slack"],
+ payload: {
+ actions: {
+ block_id: [params.blockId],
+ action_id: actionIds,
+ },
+ },
+ event: ["block.action"],
+ },
+ source: {
+ blockId: params.blockId,
+ actionIds,
+ },
+ },
+ schema: slack.schemas.blockAction,
+ };
+}
diff --git a/packages/trigger-integrations/src/integrations/slack/index.ts b/packages/trigger-integrations/src/integrations/slack/index.ts
index 1e457edcc3e..fe1fd2e2005 100644
--- a/packages/trigger-integrations/src/integrations/slack/index.ts
+++ b/packages/trigger-integrations/src/integrations/slack/index.ts
@@ -1,6 +1,9 @@
import { getTriggerRun } from "@trigger.dev/sdk";
import { z } from "zod";
import { slack } from "@trigger.dev/providers";
+import * as events from "./events";
+
+export { events };
export type PostMessageOptions = z.infer<
typeof slack.schemas.PostMessageOptionsSchema
@@ -31,3 +34,64 @@ export async function postMessage(
return output;
}
+
+export type PostMessageResponseOptions = z.infer<
+ typeof slack.schemas.PostMessageResponseOptionsSchema
+>;
+
+export type PostMessageResponseResponse = z.infer<
+ typeof slack.schemas.PostMessageResponseSuccessResponseSchema
+>;
+
+export async function postMessageResponse(
+ key: string,
+ responseUrl: string,
+ message: PostMessageResponseOptions
+): Promise {
+ const run = getTriggerRun();
+
+ if (!run) {
+ throw new Error("Cannot call postMessageResponse outside of a trigger run");
+ }
+
+ const output = await run.performRequest(key, {
+ service: "slack",
+ endpoint: "chat.postMessageResponse",
+ params: { message, responseUrl },
+ response: {
+ schema: slack.schemas.PostMessageResponseSuccessResponseSchema,
+ },
+ });
+
+ return output;
+}
+
+export type AddReactionOptions = z.infer<
+ typeof slack.schemas.AddReactionOptionsSchema
+>;
+
+export type AddReactionResponse = z.infer<
+ typeof slack.schemas.AddReactionSuccessResponseSchema
+>;
+
+export async function addReaction(
+ key: string,
+ options: AddReactionOptions
+): Promise {
+ const run = getTriggerRun();
+
+ if (!run) {
+ throw new Error("Cannot call addReaction outside of a trigger run");
+ }
+
+ const output = await run.performRequest(key, {
+ service: "slack",
+ endpoint: "reactions.add",
+ params: options,
+ response: {
+ schema: slack.schemas.AddReactionSuccessResponseSchema,
+ },
+ });
+
+ return output;
+}
diff --git a/packages/trigger-providers/src/providers/slack/index.ts b/packages/trigger-providers/src/providers/slack/index.ts
index e3149c0582c..51121adba63 100644
--- a/packages/trigger-providers/src/providers/slack/index.ts
+++ b/packages/trigger-providers/src/providers/slack/index.ts
@@ -16,6 +16,7 @@ export const slack = {
"im:write",
"mpim:write",
"chat:write.customize",
+ "reactions:write",
],
},
schemas,
diff --git a/packages/trigger-providers/src/providers/slack/interactivity.ts b/packages/trigger-providers/src/providers/slack/interactivity.ts
index 1bb3f018262..0a981c72c62 100644
--- a/packages/trigger-providers/src/providers/slack/interactivity.ts
+++ b/packages/trigger-providers/src/providers/slack/interactivity.ts
@@ -1,5 +1,4 @@
import { z } from "zod";
-import { plainTextElementSchema } from "./blocks";
const blockActionType = z.union([
z.literal("block_actions"),
@@ -23,7 +22,7 @@ export const blockAction = z.object({
channel_id: z.string(),
is_ephemeral: z.boolean(),
}),
- trigger_id: z.string(),
+ trigger_id: z.string().optional(),
team: z.object({ id: z.string(), domain: z.string() }),
enterprise: z.null(),
is_enterprise_install: z.boolean(),
@@ -32,68 +31,18 @@ export const blockAction = z.object({
.object({
bot_id: z.string(),
type: sourceType,
- text: z.string(),
- user: z.string(),
+ text: z.string().optional(),
+ user: z.string().optional(),
ts: z.string(),
- app_id: z.string(),
- blocks: z.array(
- z.union([
- z.object({
- type: z.string(),
- block_id: z.string(),
- text: z.object({
- type: z.string(),
- text: z.string(),
- verbatim: z.boolean(),
- }),
- }),
- z.object({ type: z.string(), block_id: z.string() }),
- z.object({
- type: z.string(),
- block_id: z.string(),
- text: z.object({
- type: z.string(),
- text: z.string(),
- verbatim: z.boolean(),
- }),
- accessory: z.object({
- type: z.string(),
- image_url: z.string(),
- alt_text: z.string(),
- }),
- }),
- z.object({
- type: z.string(),
- block_id: z.string(),
- elements: z.array(
- z.union([
- z.object({
- type: z.string(),
- action_id: z.string(),
- text: z.object({
- type: z.string(),
- text: z.string(),
- emoji: z.boolean(),
- }),
- value: z.string(),
- }),
- z.object({
- type: z.string(),
- action_id: z.string(),
- text: z.object({
- type: z.string(),
- text: z.string(),
- emoji: z.boolean(),
- }),
- value: z.string(),
- url: z.string(),
- }),
- ])
- ),
- }),
- ])
- ),
- team: z.string(),
+ app_id: z.string().optional(),
+ blocks: z.array(z.any()).optional(),
+ team: z.string().optional(),
+ metadata: z
+ .object({
+ event_type: z.string(),
+ event_payload: z.object({ requestId: z.string() }),
+ })
+ .optional(),
})
.optional(),
state: z.object({ values: z.object({}) }),
@@ -102,7 +51,6 @@ export const blockAction = z.object({
z.object({
action_id: z.string(),
block_id: z.string(),
- text: plainTextElementSchema,
value: z.string(),
type: z.string(),
action_ts: z.string(),
diff --git a/packages/trigger-providers/src/providers/slack/prodmanifest.json b/packages/trigger-providers/src/providers/slack/prodmanifest.json
index d67716a9202..2f63a247e93 100644
--- a/packages/trigger-providers/src/providers/slack/prodmanifest.json
+++ b/packages/trigger-providers/src/providers/slack/prodmanifest.json
@@ -18,10 +18,12 @@
"groups:read",
"im:read",
"mpim:read",
- "chat:write"
+ "chat:write",
+ "reactions:write"
],
"bot": [
- "chat:write"
+ "chat:write",
+ "reactions:write"
]
}
},
diff --git a/packages/trigger-providers/src/providers/slack/schemas.ts b/packages/trigger-providers/src/providers/slack/schemas.ts
index 7e075949f9f..c1fe80518cd 100644
--- a/packages/trigger-providers/src/providers/slack/schemas.ts
+++ b/packages/trigger-providers/src/providers/slack/schemas.ts
@@ -1,5 +1,8 @@
import { z } from "zod";
import { knownBlockSchema } from "./blocks";
+import { blockAction } from "./interactivity";
+
+export { blockAction };
export const PostMessageSuccessResponseSchema = z.object({
ok: z.literal(true),
@@ -50,6 +53,13 @@ export const PostMessageOptionsSchema = z
})
.and(ChannelNameOrIdSchema);
+export const AddReactionOptionsSchema = z
+ .object({
+ name: z.string(),
+ timestamp: z.string(),
+ })
+ .and(ChannelNameOrIdSchema);
+
export const JoinConversationSuccessResponseSchema = z.object({
ok: z.literal(true),
channel: z.object({
@@ -80,3 +90,30 @@ export const ListConversationsResponseSchema = z.discriminatedUnion("ok", [
ListConversationsSuccessResponseSchema,
ErrorResponseSchema,
]);
+
+export const PostMessageResponseOptionsSchema = z.object({
+ text: z.string().optional(),
+ blocks: z.array(knownBlockSchema).optional(),
+ response_type: z.enum(["in_channel"]).optional(),
+ replace_original: z.boolean().optional(),
+ delete_original: z.boolean().optional(),
+ thread_ts: z.string().optional(),
+});
+
+export const PostMessageResponseSuccessResponseSchema = z.object({
+ ok: z.literal(true),
+});
+
+export const PostMessageResponseResponseSchema = z.discriminatedUnion("ok", [
+ PostMessageResponseSuccessResponseSchema,
+ ErrorResponseSchema,
+]);
+
+export const AddReactionSuccessResponseSchema = z.object({
+ ok: z.literal(true),
+});
+
+export const AddReactionResponseSchema = z.discriminatedUnion("ok", [
+ AddReactionSuccessResponseSchema,
+ ErrorResponseSchema,
+]);
From e8a29dca6bcf2ab5202fa8627c2e2e293a973240 Mon Sep 17 00:00:00 2001
From: D-K-P
Date: Fri, 27 Jan 2023 11:25:29 -0800
Subject: [PATCH 22/39] Added login and sign up buttons, changed logo to point
to app.trigger.dev
---
apps/docs/mint.json | 28 +++++++++++++++-------------
apps/docs/welcome.mdx | 2 ++
2 files changed, 17 insertions(+), 13 deletions(-)
diff --git a/apps/docs/mint.json b/apps/docs/mint.json
index 9fa5c4b2284..77f128cced6 100644
--- a/apps/docs/mint.json
+++ b/apps/docs/mint.json
@@ -3,7 +3,7 @@
"logo": {
"dark": "/logo/light.png",
"light": "/logo/light.png",
- "href": "https://trigger.dev"
+ "href": "https://app.trigger.dev"
},
"favicon": "/images/favicon.png",
"colors": {
@@ -20,6 +20,16 @@
"default": "dark",
"isHidden": true
},
+ "topbarLinks": [
+ {
+ "name": "Login",
+ "url": "https://app.trigger.dev"
+ },
+ {
+ "name": "Sign up",
+ "url": "https://app.trigger.dev"
+ }
+ ],
"topbarCtaButton": {
"type": "github",
"url": "https://github.com/triggerdotdev/trigger.dev"
@@ -39,11 +49,7 @@
"navigation": [
{
"group": "Getting Started",
- "pages": [
- "welcome",
- "getting-started",
- "get-help"
- ]
+ "pages": ["welcome", "getting-started", "get-help"]
},
{
"group": "Examples",
@@ -71,15 +77,11 @@
"pages": [
{
"group": "Slack",
- "pages": [
- "integrations/apis/slack/actions/post-message"
- ]
+ "pages": ["integrations/apis/slack/actions/post-message"]
},
{
"group": "Resend.com",
- "pages": [
- "integrations/apis/resend/actions/send-email"
- ]
+ "pages": ["integrations/apis/resend/actions/send-email"]
}
]
},
@@ -138,4 +140,4 @@
"github": "https://github.com/triggerdotdev/trigger.dev",
"discord": "https://discord.gg/nkqV9xBYWy"
}
-}
\ No newline at end of file
+}
diff --git a/apps/docs/welcome.mdx b/apps/docs/welcome.mdx
index 816a75828ea..c723b28780f 100644
--- a/apps/docs/welcome.mdx
+++ b/apps/docs/welcome.mdx
@@ -8,6 +8,8 @@ Trigger.dev is an open source platform that enables developers to create event-d
Workflows live in your codebase so you can use your existing types, functions, version control and IDE. They are triggered by us but run on your server so your private data is never exposed.
+Go to [trigger.dev](https://app.trigger.dev) and sign up or login to your account.
+
_A workflow in action:_

From 74df0b702bba2804b05830c7d89f37a656ccc1c1 Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Fri, 27 Jan 2023 12:57:40 -0800
Subject: [PATCH 23/39] Fix for example workflow when mesage is undefined
---
examples/send-to-slack/src/index.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/examples/send-to-slack/src/index.ts b/examples/send-to-slack/src/index.ts
index 19c70079ac5..14cbdea4303 100644
--- a/examples/send-to-slack/src/index.ts
+++ b/examples/send-to-slack/src/index.ts
@@ -262,6 +262,11 @@ new Trigger({
blockId: BLOCK_ID_2,
}),
run: async (event, ctx) => {
+ if (!event.message) {
+ ctx.logger.debug(`No message found`);
+ return;
+ }
+
await slack.addReaction("React to message", {
name: "thumbsup",
timestamp: event.message.ts,
From b75ef0266e15954d0f747d13523c480d0284874e Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Fri, 27 Jan 2023 12:57:47 -0800
Subject: [PATCH 24/39] Added schema for blocks
---
.../trigger-providers/src/providers/slack/interactivity.ts | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/trigger-providers/src/providers/slack/interactivity.ts b/packages/trigger-providers/src/providers/slack/interactivity.ts
index 0a981c72c62..d9204fc9f93 100644
--- a/packages/trigger-providers/src/providers/slack/interactivity.ts
+++ b/packages/trigger-providers/src/providers/slack/interactivity.ts
@@ -1,4 +1,5 @@
import { z } from "zod";
+import { knownBlockSchema } from "./blocks";
const blockActionType = z.union([
z.literal("block_actions"),
@@ -35,7 +36,7 @@ export const blockAction = z.object({
user: z.string().optional(),
ts: z.string(),
app_id: z.string().optional(),
- blocks: z.array(z.any()).optional(),
+ blocks: z.array(knownBlockSchema).optional(),
team: z.string().optional(),
metadata: z
.object({
From 9f935cdd3b7e9daf12cee1433851b79542038d1f Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Fri, 27 Jan 2023 13:44:00 -0800
Subject: [PATCH 25/39] Use jsx-slack in the Slack example
---
examples/send-to-slack/package.json | 3 +-
.../send-to-slack/src/{index.ts => index.tsx} | 66 ++++++----------
examples/send-to-slack/tsconfig.json | 8 +-
pnpm-lock.yaml | 76 ++++++++++---------
4 files changed, 72 insertions(+), 81 deletions(-)
rename examples/send-to-slack/src/{index.ts => index.tsx} (86%)
diff --git a/examples/send-to-slack/package.json b/examples/send-to-slack/package.json
index 93f35229e02..1f261d4ee88 100644
--- a/examples/send-to-slack/package.json
+++ b/examples/send-to-slack/package.json
@@ -6,6 +6,7 @@
"dependencies": {
"@trigger.dev/integrations": "workspace:*",
"@trigger.dev/sdk": "workspace:*",
+ "jsx-slack": "^5.3.0",
"zod": "^3.20.2"
},
"devDependencies": {
@@ -14,6 +15,6 @@
"tsx": "^3.12.0"
},
"scripts": {
- "dev": "tsx src/index.ts"
+ "dev": "tsx src/index.tsx"
}
}
diff --git a/examples/send-to-slack/src/index.ts b/examples/send-to-slack/src/index.tsx
similarity index 86%
rename from examples/send-to-slack/src/index.ts
rename to examples/send-to-slack/src/index.tsx
index 14cbdea4303..befcaa83b93 100644
--- a/examples/send-to-slack/src/index.ts
+++ b/examples/send-to-slack/src/index.tsx
@@ -1,5 +1,8 @@
import { Trigger, customEvent } from "@trigger.dev/sdk";
import { slack } from "@trigger.dev/integrations";
+/** @jsxImportSource jsx-slack */
+import JSXSlack, { Actions, Blocks, Button, Section } from "jsx-slack";
+import { jsxslack } from "jsx-slack";
import { z } from "zod";
new Trigger({
@@ -152,48 +155,29 @@ new Trigger({
schema: z.any(),
}),
run: async (event, ctx) => {
- await slack.postMessage("send-to-slack", {
+ await slack.postMessage("jsx-test", {
channelName: "test-integrations",
- text: `Click on one of the buttons:`,
- blocks: [
- {
- type: "actions",
- block_id: BLOCK_ID,
- elements: [
- {
- type: "button",
- action_id: "close_issue_123",
- text: {
- type: "plain_text",
- text: "Close Issue",
- emoji: true,
- },
- value: "close_issue",
- },
- {
- type: "button",
- action_id: "comment_issue_123",
- text: {
- type: "plain_text",
- text: "Comment",
- emoji: true,
- },
- value: "comment_issue",
- },
- {
- type: "button",
- action_id: "view_issue_123",
- text: {
- type: "plain_text",
- text: "View Issue",
- emoji: true,
- },
- value: "view_issue",
- url: "https://github.com/triggerdotdev/trigger.dev/issues/11",
- },
- ],
- },
- ],
+ text: "Test of jsx",
+ blocks: JSXSlack(
+
+
+
+
+
+
+
+
+ ),
});
},
}).listen();
diff --git a/examples/send-to-slack/tsconfig.json b/examples/send-to-slack/tsconfig.json
index 00ad22f1a85..c441b3366ea 100644
--- a/examples/send-to-slack/tsconfig.json
+++ b/examples/send-to-slack/tsconfig.json
@@ -1,5 +1,9 @@
{
"extends": "@trigger.dev/tsconfig/examples.json",
- "include": ["src/**/*.ts"],
- "exclude": ["node_modules", "**/*.test.*"]
+ "include": ["src/**/*.ts", "src/**/*.tsx"],
+ "exclude": ["node_modules", "**/*.test.*"],
+ "compilerOptions": {
+ "jsx": "react-jsx", // or "react-jsxdev" for development
+ "jsxImportSource": "jsx-slack"
+ }
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b5ab3ac8ccd..36d67f12f19 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -184,7 +184,7 @@ importers:
'@aws-sdk/client-s3': 3.245.0
'@aws-sdk/s3-request-presigner': 3.245.0
'@cfworker/json-schema': 1.12.5
- '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu
+ '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde
'@codemirror/commands': 6.1.3
'@codemirror/lang-javascript': 6.1.2
'@codemirror/lang-json': 6.0.1
@@ -211,7 +211,7 @@ importers:
'@trigger.dev/integrations': link:../../packages/trigger-integrations
'@trigger.dev/providers': link:../../packages/trigger-providers
'@trigger.dev/sdk': link:../../packages/trigger-sdk
- '@uiw/react-codemirror': 4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle
+ '@uiw/react-codemirror': 4.19.5_aguurb4bmecpxzejz52amioxne
bcryptjs: 2.4.3
classnames: 2.3.2
clsx: 1.2.1
@@ -508,11 +508,13 @@ importers:
'@trigger.dev/sdk': workspace:*
'@trigger.dev/tsconfig': workspace:*
'@types/node': '16'
+ jsx-slack: ^5.3.0
tsx: ^3.12.0
zod: ^3.20.2
dependencies:
'@trigger.dev/integrations': link:../../packages/trigger-integrations
'@trigger.dev/sdk': link:../../packages/trigger-sdk
+ jsx-slack: 5.3.0
zod: 3.20.2
devDependencies:
'@trigger.dev/tsconfig': link:../../config-packages/tsconfig
@@ -748,7 +750,7 @@ importers:
tsup: ^6.5.0
zod: ^3.20.2
dependencies:
- '@react-email/render': 0.0.3
+ '@react-email/render': 0.0.3_react@18.2.0
'@trigger.dev/providers': link:../trigger-providers
'@trigger.dev/sdk': link:../trigger-sdk
zod: 3.20.2
@@ -3455,12 +3457,13 @@ packages:
prettier: 2.8.2
dev: false
- /@codemirror/autocomplete/6.4.0_czcfkg2f66rxeiodoti7r2gulu:
+ /@codemirror/autocomplete/6.4.0_eo6pz6bvsllvatnnwfprpuflde:
resolution: {integrity: sha512-HLF2PnZAm1s4kGs30EiqKMgD7XsYaQ0XJnMR0rofEWQ5t5D60SfqpDIkIh1ze5tiEbyUWm8+VJ6W1/erVvBMIA==}
peerDependencies:
'@codemirror/language': ^6.0.0
'@codemirror/state': ^6.0.0
'@codemirror/view': ^6.0.0
+ '@lezer/common': ^1.0.0
dependencies:
'@codemirror/language': 6.3.2
'@codemirror/state': 6.2.0
@@ -3480,7 +3483,7 @@ packages:
/@codemirror/lang-javascript/6.1.2:
resolution: {integrity: sha512-OcwLfZXdQ1OHrLiIcKCn7MqZ7nx205CMKlhe+vL88pe2ymhT9+2P+QhwkYGxMICj8TDHyp8HFKVwpiisUT7iEQ==}
dependencies:
- '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu
+ '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde
'@codemirror/language': 6.3.2
'@codemirror/lint': 6.1.0
'@codemirror/state': 6.2.0
@@ -4678,16 +4681,6 @@ packages:
react-dom: 18.2.0_react@18.2.0
dev: false
- /@react-email/render/0.0.3:
- resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==}
- engines: {node: '>=18.0.0'}
- dependencies:
- pretty: 2.0.0
- react-dom: 18.2.0
- transitivePeerDependencies:
- - react
- dev: false
-
/@react-email/render/0.0.3_react@18.2.0:
resolution: {integrity: sha512-+4eOrLGdTCJjoJU3PunekErjo3PFnhSDFjVINBHrfJT+1wlVdhWfDo7hFdzXJx/JOOYqmnZTilU/umGLdRumKQ==}
engines: {node: '>=18.0.0'}
@@ -4803,7 +4796,7 @@ packages:
eslint: 8.31.0
eslint-import-resolver-node: 0.3.6
eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy
- eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny
+ eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq
eslint-plugin-jest: 26.9.0_ohsifnwenhmxgcp7mend4dnv74
eslint-plugin-jest-dom: 4.0.3_eslint@8.31.0
eslint-plugin-jsx-a11y: 6.7.1_eslint@8.31.0
@@ -5164,6 +5157,11 @@ packages:
engines: {node: '>=10'}
dev: true
+ /@slack/types/2.8.0:
+ resolution: {integrity: sha512-ghdfZSF0b4NC9ckBA8QnQgC9DJw2ZceDq0BIjjRSv6XAZBXJdWgxIsYz0TYnWSiqsKZGH2ZXbj9jYABZdH3OSQ==}
+ engines: {node: '>= 12.13.0', npm: '>= 6.12.0'}
+ dev: false
+
/@swc/core-darwin-arm64/1.3.26:
resolution: {integrity: sha512-FWWflBfKRYrUJtko2xiedC5XCa31O75IZZqnTWuLpe9g3C5tnUuF3M8LSXZS/dn6wprome1MhtG9GMPkSYkhkg==}
engines: {node: '>=10'}
@@ -5918,17 +5916,18 @@ packages:
eslint-visitor-keys: 3.3.0
dev: true
- /@uiw/codemirror-extensions-basic-setup/4.19.5_wd2tsis3in55bkaiwnc2c46tom:
+ /@uiw/codemirror-extensions-basic-setup/4.19.5_tbeldtdcrf45b35pezgkzq2u4e:
resolution: {integrity: sha512-1zt7ZPJ01xKkSW/KDy0FZNga0bngN1fC594wCVG7FBi60ehfcAucpooQ+JSPScKXopxcb+ugPKZvVLzr9/OfzA==}
peerDependencies:
'@codemirror/autocomplete': '>=6.0.0'
'@codemirror/commands': '>=6.0.0'
'@codemirror/language': '>=6.0.0'
+ '@codemirror/lint': '>=6.0.0'
'@codemirror/search': '>=6.0.0'
'@codemirror/state': '>=6.0.0'
'@codemirror/view': '>=6.0.0'
dependencies:
- '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu
+ '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde
'@codemirror/commands': 6.1.3
'@codemirror/language': 6.3.2
'@codemirror/lint': 6.1.0
@@ -5937,11 +5936,14 @@ packages:
'@codemirror/view': 6.7.2
dev: false
- /@uiw/react-codemirror/4.19.5_k4ec5g7vuuzzonc3d6xbjnmmle:
+ /@uiw/react-codemirror/4.19.5_aguurb4bmecpxzejz52amioxne:
resolution: {integrity: sha512-ZCHh8d7beXbF8/t7F1+yHht6A9Y6CdKeOkZq4A09lxJEnyTQrj1FMf2zvfaqc7K23KNjkTCtSlbqKKbVDgrWaw==}
peerDependencies:
+ '@babel/runtime': '>=7.11.0'
'@codemirror/state': '>=6.0.0'
+ '@codemirror/theme-one-dark': '>=6.0.0'
'@codemirror/view': '>=6.0.0'
+ codemirror: '>=6.0.0'
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
@@ -5950,13 +5952,14 @@ packages:
'@codemirror/state': 6.2.0
'@codemirror/theme-one-dark': 6.1.0
'@codemirror/view': 6.7.2
- '@uiw/codemirror-extensions-basic-setup': 4.19.5_wd2tsis3in55bkaiwnc2c46tom
- codemirror: 6.0.1
+ '@uiw/codemirror-extensions-basic-setup': 4.19.5_tbeldtdcrf45b35pezgkzq2u4e
+ codemirror: 6.0.1_@lezer+common@1.0.2
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
transitivePeerDependencies:
- '@codemirror/autocomplete'
- '@codemirror/language'
+ - '@codemirror/lint'
- '@codemirror/search'
dev: false
@@ -7267,16 +7270,18 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
- /codemirror/6.0.1:
+ /codemirror/6.0.1_@lezer+common@1.0.2:
resolution: {integrity: sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==}
dependencies:
- '@codemirror/autocomplete': 6.4.0_czcfkg2f66rxeiodoti7r2gulu
+ '@codemirror/autocomplete': 6.4.0_eo6pz6bvsllvatnnwfprpuflde
'@codemirror/commands': 6.1.3
'@codemirror/language': 6.3.2
'@codemirror/lint': 6.1.0
'@codemirror/search': 6.2.3
'@codemirror/state': 6.2.0
'@codemirror/view': 6.7.2
+ transitivePeerDependencies:
+ - '@lezer/common'
dev: false
/collection-visit/1.0.0:
@@ -8612,7 +8617,7 @@ packages:
debug: 4.3.4
enhanced-resolve: 5.12.0
eslint: 8.31.0
- eslint-plugin-import: 2.27.4_2ac3tknkazjoq5fxmuugu665ny
+ eslint-plugin-import: 2.27.4_qdjeohovcytra7xto5vgmxssaq
get-tsconfig: 4.3.0
globby: 13.1.3
is-core-module: 2.11.0
@@ -8622,7 +8627,7 @@ packages:
- supports-color
dev: true
- /eslint-module-utils/2.7.4_v73lhamtbyinynmwa5fn7kpmfq:
+ /eslint-module-utils/2.7.4_sqt5xxn4ciiurbqrzlaarm6ama:
resolution: {integrity: sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==}
engines: {node: '>=4'}
peerDependencies:
@@ -8647,7 +8652,6 @@ packages:
debug: 3.2.7
eslint: 8.31.0
eslint-import-resolver-node: 0.3.7
- eslint-import-resolver-typescript: 3.5.3_hnftvkj7qg3s6bbigj4pr6djxy
transitivePeerDependencies:
- supports-color
dev: true
@@ -8672,7 +8676,7 @@ packages:
regexpp: 3.2.0
dev: true
- /eslint-plugin-import/2.27.4_2ac3tknkazjoq5fxmuugu665ny:
+ /eslint-plugin-import/2.27.4_qdjeohovcytra7xto5vgmxssaq:
resolution: {integrity: sha512-Z1jVt1EGKia1X9CnBCkpAOhWy8FgQ7OmJ/IblEkT82yrFU/xJaxwujaTzLWqigewwynRQ9mmHfX9MtAfhxm0sA==}
engines: {node: '>=4'}
peerDependencies:
@@ -8690,7 +8694,7 @@ packages:
doctrine: 2.1.0
eslint: 8.31.0
eslint-import-resolver-node: 0.3.7
- eslint-module-utils: 2.7.4_v73lhamtbyinynmwa5fn7kpmfq
+ eslint-module-utils: 2.7.4_sqt5xxn4ciiurbqrzlaarm6ama
has: 1.0.3
is-core-module: 2.11.0
is-glob: 4.0.3
@@ -11184,6 +11188,13 @@ packages:
object.assign: 4.1.4
dev: true
+ /jsx-slack/5.3.0:
+ resolution: {integrity: sha512-xPYP8zLO31AKYiyuDJ88ykJzV/GvZUK+ktM4xE6ESIzrCICSd8nhrW/yaSyX45U+oD5LNTDUnhJvQe0LFDi63g==}
+ engines: {node: '>=14'}
+ dependencies:
+ '@slack/types': 2.8.0
+ dev: false
+
/junk/3.1.0:
resolution: {integrity: sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ==}
engines: {node: '>=8'}
@@ -13779,15 +13790,6 @@ packages:
shallow-equal: 1.2.1
dev: false
- /react-dom/18.2.0:
- resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
- peerDependencies:
- react: ^18.2.0
- dependencies:
- loose-envify: 1.4.0
- scheduler: 0.23.0
- dev: false
-
/react-dom/18.2.0_react@18.2.0:
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies:
From ffacc2fb6d6ba0d6214865c0aa3526447607374c Mon Sep 17 00:00:00 2001
From: Matt Aitken
Date: Fri, 27 Jan 2023 14:58:32 -0800
Subject: [PATCH 26/39] Added a static select dropdown and lots more schema
work
---
examples/send-to-slack/src/index.tsx | 44 ++++-
.../src/providers/slack/interactivity.ts | 181 +++++++++++++-----
2 files changed, 178 insertions(+), 47 deletions(-)
diff --git a/examples/send-to-slack/src/index.tsx b/examples/send-to-slack/src/index.tsx
index befcaa83b93..f09508f837c 100644
--- a/examples/send-to-slack/src/index.tsx
+++ b/examples/send-to-slack/src/index.tsx
@@ -1,8 +1,13 @@
import { Trigger, customEvent } from "@trigger.dev/sdk";
import { slack } from "@trigger.dev/integrations";
-/** @jsxImportSource jsx-slack */
-import JSXSlack, { Actions, Blocks, Button, Section } from "jsx-slack";
-import { jsxslack } from "jsx-slack";
+import JSXSlack, {
+ Actions,
+ Blocks,
+ Button,
+ Section,
+ Select,
+ Option,
+} from "jsx-slack";
import { z } from "zod";
new Trigger({
@@ -143,6 +148,7 @@ function getRandomQuote() {
}
const BLOCK_ID = "issue.action.block";
+const BLOCK_ID_RATING = "issue.rating.block";
new Trigger({
id: "slack-interactivity",
@@ -176,6 +182,16 @@ new Trigger({
Comment
+
+ Rate this experience
+
+
),
});
@@ -236,6 +252,28 @@ new Trigger({
},
}).listen();
+new Trigger({
+ id: "slack-block-interaction-rating",
+ name: "Slack get rating",
+ apiKey: "trigger_dev_zC25mKNn6c0q",
+ endpoint: "ws://localhost:8889/ws",
+ logLevel: "debug",
+ on: slack.events.blockActionInteraction({
+ blockId: BLOCK_ID_RATING,
+ actionId: ["rating"],
+ }),
+ run: async (event, ctx) => {
+ const ratingAction = event.actions.find((a) => a.action_id === "rating");
+
+ if (ratingAction?.type === "static_select") {
+ await slack.postMessageResponse("Response to user", event.response_url, {
+ text: `Thanks for rating ${ratingAction.selected_option.value}!`,
+ replace_original: false,
+ });
+ }
+ },
+}).listen();
+
new Trigger({
id: "slack-block-interaction-2",
name: "Slack Block Interaction 2",
diff --git a/packages/trigger-providers/src/providers/slack/interactivity.ts b/packages/trigger-providers/src/providers/slack/interactivity.ts
index d9204fc9f93..254d259ab3b 100644
--- a/packages/trigger-providers/src/providers/slack/interactivity.ts
+++ b/packages/trigger-providers/src/providers/slack/interactivity.ts
@@ -1,5 +1,12 @@
import { z } from "zod";
-import { knownBlockSchema } from "./blocks";
+import {
+ blockSchema,
+ knownBlockSchema,
+ messageAttachmentSchema,
+ mrkdwnElementSchema,
+ plainTextElementSchema,
+ viewSchema,
+} from "./blocks";
const blockActionType = z.union([
z.literal("block_actions"),
@@ -8,53 +15,139 @@ const blockActionType = z.union([
const sourceType = z.literal("message");
-export const blockAction = z.object({
- type: blockActionType,
- user: z.object({
- id: z.string(),
- username: z.string(),
- name: z.string(),
- team_id: z.string(),
- }),
- api_app_id: z.string(),
- container: z.object({
- type: sourceType,
- message_ts: z.string(),
- channel_id: z.string(),
- is_ephemeral: z.boolean(),
+const commonActionSchema = z.object({
+ action_id: z.string(),
+ block_id: z.string(),
+ action_ts: z.string(),
+});
+
+const buttonAction = z.object({
+ type: z.literal("button"),
+ value: z.string(),
+});
+
+const staticSelectAction = z.object({
+ type: z.literal("static_select"),
+ selected_option: z.object({
+ text: z.object({
+ type: z.string(),
+ text: z.string(),
+ emoji: z.boolean().optional(),
+ }),
+ value: z.string(),
}),
- trigger_id: z.string().optional(),
- team: z.object({ id: z.string(), domain: z.string() }),
- enterprise: z.null(),
- is_enterprise_install: z.boolean(),
- channel: z.object({ id: z.string(), name: z.string() }),
- message: z
+ placeholder: z
.object({
- bot_id: z.string(),
- type: sourceType,
- text: z.string().optional(),
- user: z.string().optional(),
- ts: z.string(),
- app_id: z.string().optional(),
- blocks: z.array(knownBlockSchema).optional(),
- team: z.string().optional(),
- metadata: z
- .object({
- event_type: z.string(),
- event_payload: z.object({ requestId: z.string() }),
- })
- .optional(),
+ type: z.string(),
+ text: z.string(),
+ emoji: z.boolean(),
})
.optional(),
- state: z.object({ values: z.object({}) }),
- response_url: z.string(),
- actions: z.array(
- z.object({
- action_id: z.string(),
- block_id: z.string(),
- value: z.string(),
- type: z.string(),
- action_ts: z.string(),
+});
+
+const actionSchema = z
+ .discriminatedUnion("type", [buttonAction, staticSelectAction])
+ .and(commonActionSchema);
+
+const userSchema = z.object({
+ id: z.string(),
+ username: z.string(),
+ name: z.string(),
+ team_id: z.string(),
+});
+
+const containerSchema = z.object({
+ type: sourceType,
+ message_ts: z.string(),
+ channel_id: z.string(),
+ is_ephemeral: z.boolean(),
+});
+
+const teamSchema = z.object({ id: z.string(), domain: z.string() });
+const channelSchema = z.object({ id: z.string(), name: z.string() });
+
+const viewActionDataSchema = z.object({
+ id: z.string(),
+ team_id: z.string(),
+ state: z
+ .object({
+ values: z.record(z.record(z.any())),
})
+ .optional(),
+ hash: z.string(),
+ previous_view_id: z.string().optional(),
+ root_view_id: z.string().optional(),
+ app_id: z.string().optional(),
+ app_installed_team_id: z.string().optional(),
+ bot_id: z.string().optional(),
+});
+const viewActionSchema: Zod.ZodIntersection<
+ typeof viewSchema,
+ typeof viewActionDataSchema
+> = viewSchema.and(viewActionDataSchema);
+
+const messageActionSchema = z.object({
+ bot_id: z.string(),
+ type: sourceType,
+ text: z.string().optional(),
+ user: z.string().optional(),
+ ts: z.string(),
+ app_id: z.string().optional(),
+ blocks: z.array(knownBlockSchema).optional(),
+ team: z.string().optional(),
+ metadata: z
+ .object({
+ event_type: z.string(),
+ event_payload: z.object({ requestId: z.string() }),
+ })
+ .optional(),
+});
+
+const stateSchema = z.object({
+ values: z.record(
+ z.record(
+ z.object({
+ type: z.string(),
+ text: z
+ .discriminatedUnion("type", [
+ plainTextElementSchema,
+ mrkdwnElementSchema,
+ ])
+ .optional(),
+ value: z.string().optional(),
+ })
+ )
),
});
+
+export const blockAction: Zod.ZodObject<{
+ type: typeof blockActionType;
+ user: typeof userSchema;
+ api_app_id: Zod.ZodString;
+ container: typeof containerSchema;
+ trigger_id: z.ZodOptional;
+ team: typeof teamSchema;
+ enterprise: Zod.ZodAny;
+ is_enterprise_install: Zod.ZodBoolean;
+ channel: typeof channelSchema;
+ view: z.ZodOptional;
+ message: z.ZodOptional;
+ state: z.ZodOptional;
+ response_url: Zod.ZodString;
+ actions: Zod.ZodArray;
+}> = z.object({
+ type: blockActionType,
+ user: userSchema,
+ api_app_id: z.string(),
+ container: containerSchema,
+ trigger_id: z.string().optional(),
+ team: teamSchema,
+ enterprise: z.any(),
+ is_enterprise_install: z.boolean(),
+ channel: channelSchema,
+ view: viewActionSchema.optional(),
+ message: messageActionSchema.optional(),
+ state: stateSchema.optional(),
+ response_url: z.string(),
+ actions: z.array(actionSchema),
+});
From 617cc221cbffc7080c517589e910d929924950ea Mon Sep 17 00:00:00 2001
From: James Ritchie
Date: Fri, 27 Jan 2023 15:11:59 -0800
Subject: [PATCH 27/39] Added new slack integration workflow type
---
.../images/triggers/slack-interaction.png | Bin 0 -> 46640 bytes
.../app/components/triggers/Trigger.tsx | 40 ++++++++++++++++++
.../app/components/triggers/TriggerIcons.tsx | 9 ++++
3 files changed, 49 insertions(+)
create mode 100644 apps/webapp/app/assets/images/triggers/slack-interaction.png
diff --git a/apps/webapp/app/assets/images/triggers/slack-interaction.png b/apps/webapp/app/assets/images/triggers/slack-interaction.png
new file mode 100644
index 0000000000000000000000000000000000000000..9ea87cfe05e4d936cf34aa6ad94e68aa8ac0d0fb
GIT binary patch
literal 46640
zcmV)NK)1h%P)8W5uhdXfrLK%@N!)bSypN=@@{YfmFe|o(ecGr6U0Kz=H>9ZqFR;|LJ}@ynBvj^Y6oS
zI9{8JzYjckkOX}PwjM0tBeu>B6z>v|!1CW1xNp(lG!(4XFb5ozb#h7=`^E
zGyv8N#4QJCUj*sipX{T9TjTfJ_h;~?UJ~4-4&zWilZ6Z+f95OsVuo+vJ{9_J(9dAk
zQyHOFls~fu2+Ll$j21*-FFR)$Enc@Mf2O~5EkDDx49?{x_j@yU{C5*`6Pv@DV=nkp
zosI$cDN0ee*HZ$mSy)X8tb1?Ymn~}g+1uTEz@@SzK(aH-}W!4)OXgaD;xkDKB!JGuxZ|H|`M(s8d}6Rq?LT6X5L$}?r&
z6|vaATXf>0Pt&pVxYIEJKaK?MaRRP_cK3C=X-5R?%(r&N@$78P3;+x;UPLgbRIOMY
zm=3tpP*{Lxm>a;>UxI$PgkRC>JD@Weo$a#&ZoO}Zx$GoXyxv)S(jr=Q{wg~Dt;f;I
zm#@UKXQHouMc2)JX6d@6o9I}2Oz9Yak1|zj`@#74ArjC=0Zil91lotc{77__57%*H
zQ1=uu+Pb3cEx9JoB`A!lWfoU;UTdlOhVK&462wwKVJZU<2aXgc2wd4;#G&X0HIM
zG6|!gz+QaPVp@6LN;>{65zObUlE$X2(>%wQ(FARZV4kL9>4@nVfRB)>wN1c%r(8>8
z+O2=RErT{q>7dI?^}B(ZCS~ph~3*s9GF(8wbz0AXHyD^j+eL$ZY8uJOs@z
z;W472@$XQ1p{=EsGLkBodK6YAu;k<=wD!%%(Ft!ko)(_CsDe4=4xgq&-Dj4bdCV%$
zmyQ8=Op4%1fK9+%FV~WbeE*+4m_Kt0eJcv?WHMAaN4CgAb&z_@)WEX2qNP{bYTyU~
zTa`ozFs|)FG2xA6<_3@<-3}a%G;;P-h~>pQ)VJ_WUC!+X%bos7uJ!S6UQ5SsSgSfO
z4yQLC4*c;Ir!W099ZS7*48V}UO@1HMxmvNd+u~nZI8pLZV+zZ>YXISdQgdb(d#YAR
z5(a`?m#ANA7L%JS^1MWhjRYUL4$U}L$ERHhQmK;=&|%78;m(jUCbg
z^*Ox@>SJ-V#3#M|BwBOvYFc!{B6)7Q)ySN7N4+CfgJB$w&Lt#Ku+mr9>7r;K0*N8
zUC}*$AiBxB#7)MrYx)V637y`)8yYP5GhmZ}05_C%cyyMKe0c*AZF6nKO>W1fnQB!B
zy^(7mH?1{j@zJvShp3%XV)kyfT74dN(`k(2Nzbiae;l3i8>bYDT(ahX*NSt0wt@UJ
z3*abJ?~5M^yi@77jBg!qQv%D@O1#Yc$Sr~=TPtXm70}uOyMolB*nzq(8ps$7>e_KD
zIhc|9pj53FH3_QgD-a)@R$3BxMk5>NqU8?Am?4`wY-Nf1%ELe&dUsKuBqh0a{n~Kq
zZ>&=wM}0KiMIf(w$FY{h2!8A?Ah0Iqv#KT35vD6NdT_oMg4
zG44YqEIokXxUT$eY#!PP>WQBVGdCj76@4pcd(W7N1{XsR+yrIe
z@Zbt3??Vz!KpwAAxhTW9CUvP6|ouu0OW&Wh*bE=XrWw130_OOdpG_ydeGFvFXuhb?>FwEeG3-y7NXa
z(XfTZYw1_#qO1ggC?>a^N8D;IyNSx|O;rj`nr$vu$GxIQtX&?2N_cm447?=_4k-LWzxba^dZfdhp0ElU6riPt;6$Rsgo5>AYS{1wPMz%
zyNPbYnwPAZrsrvTUIMsI3w?x&)y*5;@A_ZvqMg^3T|h1g!0JaBQ6szzLe&>I18td3
zRpDi@-nAGo^H6uW>84$%Q6Mhp6+BI{-crg1u)AlWQ5J|QMA#C_lGlP@eHCWjD=0O2
zmTZSrq04EAhHj>wCLnT}r=AD`j!UBuTAZf{d;GHc6_a%O@4SE(tzA@`Kl)fVclfe3
z7p-~T?SOjT0k{UKJ{A9_((ea8`CxRHx8?;ID0C`l@*K6!VE~K4&I6`iz6OzsHFI-S
zFk5=-Kdoz6+v0$$zzY634S@Bgc&InDHoC_o97|KnopE-ToQv(ekiI(7{2^=04&9bY
zGmopr(SxeR;lHL+-nov}y<=Tr0>^2mL;S8auU_-K3UWQK0316$1*x*tP0{5$F1s_I
za4{sZp*IE{u#_~P48hgLpm>AF>iVJQiv^>lRBOhLNC<{CR-8P@bu8Q94TE1|?F;5T
zSg@_a1+%zTcP<};V>q5h{tQ{!hL3t%-C>i`lvgUbN8S|or)lwRh
zhn5A*<)BMBAmyCH!~s1{7&AA3(h{4^_)XnVGjdS#2DE@p;$HrNHusv*Xew}r158!5
zbvm6TzmpCe(cl2Rt;$F#m7emeC)2vOpDf(6af$F9i>Dh^;BRb{n
z>*$0HC&-f@8!e*eY=G-I3E;XYmGHCt{%L^II{83XqztDgici!`_>2WM0Kx
zuIoBvb2Ixw<$OddED2`k1-sBFM3ww*!#*^@ttDoxr}YrpZ((d+b?3P(mvpCrcvryl
zi99OQ6^5FKA#{h+SEXW>Xt7hJZk#}4J#uze?T%ZhLNq)K00nvCTTY-;eq~(-bo@De
zX!hWSbsL_0F7M^0VeaDT(bbMj+2%NR!frc85?r!52k28x(
zg=9K?)m7wc}Zlw>&HPvVA0gytJeqmI}rIXEt&K+0!-lI_S`fyUhOkj
zE~Awn7$0lZJZO!aqH#zWhOm7Hm&2^RYpY%apbqbK_K87B+V5VIhRbMpGFAyOSXoy&
z4-6&r7F5B`yO>zEfL`$Hr_gb)IZpVp@q|m(UPjN6^qc`OP7$TC?)`uE006fB6M~O!
zN&?I2Ju5Hl3_sM>E|pt3L#`_#i#+Ja;RL(@M?>(RFfHOOA32>dZW_f|Ss@Ty)V>Cx
z-gg!B)}I|f;8>s{2C03<0HvOT=J(u@FBss{N}y8<{;X~NQW}K-p3KR=d=jnu#gir5
z$4+>|2_L5CKzh~#xCX0|%e+2~cc=gJy|n$y51BS)f`F{Ok-%A5EsRK~Uj~9BP27aq
z0GNs@Lmi%2Pkd;w1D{fU%~L8DGoRJLt!FiWF<6yeJM!cI`|bIR<<|Yke)Q0z^f%kT
zNOwm7W7)4*`AYhg6W&Hk!jgJ@Xe>%vPS=5w12;Pi(Z(LIxFi#m5dJnrG*Dt~Q!XbM
z@4V9>%Q2I}=%_c|=c+ubGG2V#B0A^)b~Y_qlc$=dJDPh|2e+QJ0CwPg@BrQNfgkC6
zf&jZ7r_N%MAD@D|G@O5a_th2H2x9u;r*(=POSBd1FV<%yXsTyv)gsFS*}Tz!m2F+(
zAkp5o&D0~@xx=Z9|Mi~#56wgn_sWjxtZ0dU_mtlw8n9($hK$`nH+ckZT8IE@t=3`i
zFvygmogvLFLI+fstq$|+vONnzuLZbndyusJ^y0OPD~R(kKIzSj&sq?V=vkN|gVS3M
zGI-M)4sLwU%>}$vdZ);G{#83jfFT8t_