├── .slack └── apps.json ├── functions ├── handle_interactivity_test.ts ├── handle_interactive_blocks_test.ts ├── handle_message_interactivity_test.ts ├── print_inputs_test.ts ├── build_message_text_test.ts ├── create_message_template_test.ts ├── print_inputs.ts ├── send_metadata_message_test.ts ├── build_message_text.ts ├── verify_channel_id_test.ts ├── send_message_if_any_test.ts ├── verify_channel_id.ts ├── create_message_template.ts ├── send_message_if_any.ts ├── send_metadata_message.ts ├── modals.ts ├── handle_interactive_blocks.ts ├── handle_interactivity.ts ├── handle_message_interactivity.ts └── manage_reaction_added_event_trigger.ts ├── deno.jsonc ├── README.md ├── .gitignore ├── assets └── icon.png ├── slack.json ├── import_map.json ├── .vscode └── settings.json ├── utils ├── function_source_file.ts └── logger.ts ├── triggers ├── interactivity │ ├── datastore.ts │ ├── setup_reaction_added_event_triggers.ts │ ├── message_metadata_sender.ts │ ├── link.ts │ ├── interactivity.ts │ └── interactive_blocks.ts ├── webhooks │ └── trigger.ts ├── events │ ├── app_mentioned.ts │ ├── reaction_added.ts │ └── message_metadata_posted.ts └── scheduled │ └── trigger.ts ├── event_types └── incident.ts ├── .github └── workflows │ └── deno.yml ├── workflows ├── reaction_added_event_workflow.ts ├── interactivity_workflow.ts ├── message_metadata_sender_workflow.ts ├── reaction_added_event_setup_workflow.ts ├── message_metadata_receiver_workflow.ts ├── channel_event_workflow.ts ├── link_trigger_workflow.ts ├── datastore_workflow.ts └── interactive_blocks_workflow.ts ├── LICENSE ├── datastores └── message_templates.ts └── manifest.ts /.slack/apps.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /functions/handle_interactivity_test.ts: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /functions/handle_interactive_blocks_test.ts: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /functions/handle_message_interactivity_test.ts: -------------------------------------------------------------------------------- 1 | // TODO 2 | -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "importMap": "import_map.json" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # My run-on-slack app template 2 | 3 | (work in progress) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | package 3 | .DS_Store 4 | .slack/apps.dev.json 5 | .env* 6 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seratch/run-on-slack-template/HEAD/assets/icon.png -------------------------------------------------------------------------------- /slack.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "get-hooks": "deno run -q --allow-read --allow-net https://deno.land/x/deno_slack_hooks@0.4.0/mod.ts" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /import_map.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "deno-slack-sdk/": "https://deno.land/x/deno_slack_sdk@1.2.0/", 4 | "deno-slack-api/": "https://deno.land/x/deno_slack_api@1.2.0/" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.suggest.imports.hosts": { 5 | "https://deno.land": false 6 | }, 7 | "[typescript]": { 8 | "editor.formatOnSave": true, 9 | "editor.defaultFormatter": "denoland.vscode-deno" 10 | }, 11 | "editor.tabSize": 2 12 | } 13 | -------------------------------------------------------------------------------- /functions/print_inputs_test.ts: -------------------------------------------------------------------------------- 1 | import { SlackFunctionTester } from "deno-slack-sdk/mod.ts"; 2 | import handler from "./print_inputs.ts"; 3 | 4 | const { createContext } = SlackFunctionTester("my-function"); 5 | const env = { logLevel: "CRITICAL" }; 6 | 7 | Deno.test("Print all inputs", async () => { 8 | const inputs = { id: "xxx", name: "name", text: "foo" }; 9 | await handler(createContext({ inputs, env })); 10 | }); 11 | -------------------------------------------------------------------------------- /utils/function_source_file.ts: -------------------------------------------------------------------------------- 1 | export const FunctionSourceFile = function ( 2 | // Pass the value of import.meta.url in a function code 3 | importMetaUrl: string, 4 | // If you have sub diretories under "functions" dir, set the depth. 5 | // When you place functions/pto/data_submission.ts, the depth for the source file is 1. 6 | depth = 0, 7 | ): string { 8 | const sliceStart = -2 - depth; 9 | const path = new URL("", importMetaUrl).pathname; 10 | return path.split("/").slice(sliceStart).join("/"); 11 | }; 12 | -------------------------------------------------------------------------------- /triggers/interactivity/datastore.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import workflowDef from "../../workflows/datastore_workflow.ts"; 3 | 4 | /** 5 | * See https://api.slack.com/future/triggers/link 6 | */ 7 | const trigger: Trigger = { 8 | type: "shortcut", 9 | name: "Datastore trigger", 10 | workflow: `#/workflows/${workflowDef.definition.callback_id}`, 11 | inputs: { 12 | interactivity: { value: "{{data.interactivity}}" }, 13 | }, 14 | }; 15 | 16 | export default trigger; 17 | -------------------------------------------------------------------------------- /utils/logger.ts: -------------------------------------------------------------------------------- 1 | import * as log from "https://deno.land/std@0.157.0/log/mod.ts"; 2 | 3 | export const Logger = function ( 4 | level?: string, 5 | ): log.Logger { 6 | const logLevel: log.LevelName = level === undefined 7 | ? "DEBUG" 8 | : level as log.LevelName; 9 | log.setup({ 10 | handlers: { 11 | console: new log.handlers.ConsoleHandler(logLevel), 12 | }, 13 | loggers: { 14 | default: { 15 | level: logLevel, 16 | handlers: ["console"], 17 | }, 18 | }, 19 | }); 20 | return log.getLogger(); 21 | }; 22 | -------------------------------------------------------------------------------- /event_types/incident.ts: -------------------------------------------------------------------------------- 1 | import { DefineEvent, Schema } from "deno-slack-sdk/mod.ts"; 2 | 3 | const IncidentEvent = DefineEvent({ 4 | name: "my_incident_event", 5 | title: "Incident", 6 | type: Schema.types.object, 7 | properties: { 8 | id: { type: Schema.types.string }, 9 | title: { type: Schema.types.string }, 10 | summary: { type: Schema.types.string }, 11 | severity: { type: Schema.types.string }, 12 | }, 13 | required: ["id", "title", "summary", "severity"], 14 | additionalProperties: false, 15 | }); 16 | export default IncidentEvent; 17 | -------------------------------------------------------------------------------- /triggers/interactivity/setup_reaction_added_event_triggers.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import workflowDef from "../../workflows/reaction_added_event_setup_workflow.ts"; 3 | 4 | /** 5 | * See https://api.slack.com/future/triggers/link 6 | */ 7 | const trigger: Trigger = { 8 | type: "shortcut", 9 | name: "Set up reaction_added event trigger", 10 | workflow: `#/workflows/${workflowDef.definition.callback_id}`, 11 | inputs: { 12 | interactivity: { value: "{{data.interactivity}}" }, 13 | }, 14 | }; 15 | 16 | export default trigger; 17 | -------------------------------------------------------------------------------- /triggers/webhooks/trigger.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import workflowDef from "../../workflows/channel_event_workflow.ts"; 3 | 4 | /** 5 | * See https://api.slack.com/future/triggers/webhook 6 | * required scopes: reactions:read 7 | */ 8 | const trigger: Trigger = { 9 | type: "webhook", 10 | name: "Incoming webhook trigger", 11 | workflow: `#/workflows/${workflowDef.definition.callback_id}`, 12 | inputs: { 13 | channelId: { 14 | value: "{{data.channel_id}}", 15 | }, 16 | }, 17 | }; 18 | 19 | export default trigger; 20 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | name: Deno 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | deno: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Setup repo 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Deno 18 | uses: denoland/setup-deno@v1 19 | with: 20 | deno-version: v1.x 21 | 22 | - name: Verify formatting 23 | run: deno fmt --check 24 | 25 | - name: Run linter 26 | run: deno lint 27 | 28 | - name: Run tests 29 | run: deno test -------------------------------------------------------------------------------- /triggers/interactivity/message_metadata_sender.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import workflowDef from "../../workflows/message_metadata_sender_workflow.ts"; 3 | 4 | console.log(workflowDef.definition.callback_id); 5 | /** 6 | * See https://api.slack.com/future/triggers/link 7 | */ 8 | const trigger: Trigger = { 9 | type: "shortcut", 10 | name: "Sample trigger", 11 | workflow: `#/workflows/${workflowDef.definition.callback_id}`, 12 | inputs: { 13 | channelId: { value: "{{data.channel_id}}" }, 14 | }, 15 | }; 16 | 17 | export default trigger; 18 | -------------------------------------------------------------------------------- /triggers/interactivity/link.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import workflowDef from "../../workflows/link_trigger_workflow.ts"; 3 | 4 | console.log(workflowDef.definition.callback_id); 5 | /** 6 | * See https://api.slack.com/future/triggers/link 7 | */ 8 | const trigger: Trigger = { 9 | type: "shortcut", 10 | name: "Sample trigger", 11 | workflow: `#/workflows/${workflowDef.definition.callback_id}`, 12 | inputs: { 13 | interactivity: { value: "{{data.interactivity}}" }, 14 | channel: { value: "{{data.channel_id}}" }, 15 | }, 16 | }; 17 | 18 | export default trigger; 19 | -------------------------------------------------------------------------------- /workflows/reaction_added_event_workflow.ts: -------------------------------------------------------------------------------- 1 | import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; 2 | 3 | /** 4 | * https://api.slack.com/future/workflows 5 | */ 6 | const workflow = DefineWorkflow({ 7 | callback_id: "reaction-added-event-workflow", 8 | title: "The reaction_addd Event Workflow", 9 | input_parameters: { 10 | properties: { 11 | channelId: { type: Schema.slack.types.channel_id }, 12 | messageTs: { type: Schema.types.string }, 13 | reaction: { type: Schema.types.string }, 14 | }, 15 | required: ["channelId", "messageTs", "reaction"], 16 | }, 17 | }); 18 | 19 | export default workflow; 20 | -------------------------------------------------------------------------------- /triggers/interactivity/interactivity.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import workflowDef from "../../workflows/interactivity_workflow.ts"; 3 | 4 | /** 5 | * See https://api.slack.com/future/triggers/link 6 | */ 7 | const trigger: Trigger = { 8 | type: "shortcut", 9 | name: "Interactivity Trigger", 10 | workflow: `#/workflows/${workflowDef.definition.callback_id}`, 11 | inputs: { 12 | interactivity: { value: "{{data.interactivity}}" }, 13 | channel: { value: "{{data.channel_id}}" }, 14 | user: { value: "{{data.user_id}}" }, 15 | }, 16 | }; 17 | 18 | export default trigger; 19 | -------------------------------------------------------------------------------- /triggers/interactivity/interactive_blocks.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import workflowDef from "../../workflows/interactive_blocks_workflow.ts"; 3 | 4 | /** 5 | * See https://api.slack.com/future/triggers/link 6 | */ 7 | const trigger: Trigger = { 8 | type: "shortcut", 9 | name: "Interactive Blocks Trigger", 10 | workflow: `#/workflows/${workflowDef.definition.callback_id}`, 11 | inputs: { 12 | interactivity: { value: "{{data.interactivity}}" }, 13 | channel: { value: "{{data.channel_id}}" }, 14 | user: { value: "{{data.user_id}}" }, 15 | }, 16 | }; 17 | 18 | export default trigger; 19 | -------------------------------------------------------------------------------- /functions/build_message_text_test.ts: -------------------------------------------------------------------------------- 1 | import { SlackFunctionTester } from "deno-slack-sdk/mod.ts"; 2 | import { assertEquals } from "https://deno.land/std@0.153.0/testing/asserts.ts"; 3 | import handler from "./build_message_text.ts"; 4 | 5 | const { createContext } = SlackFunctionTester("my-function"); 6 | const env = { logLevel: "CRITICAL" }; 7 | 8 | Deno.test("Transform a message", async () => { 9 | const inputs = { messageText: "Hey, how are you doing?" }; 10 | const { outputs } = await handler(createContext({ inputs, env })); 11 | assertEquals( 12 | outputs?.updatedMessageText, 13 | ":wave: You submitted the following message: \n\n>Hey, how are you doing?", 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /workflows/interactivity_workflow.ts: -------------------------------------------------------------------------------- 1 | import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; 2 | import { def as handleInteractivity } from "../functions/handle_interactivity.ts"; 3 | 4 | /** 5 | * https://api.slack.com/future/workflows 6 | */ 7 | const workflow = DefineWorkflow({ 8 | callback_id: "block-actions-workflow", 9 | title: "Block Actions Workflow", 10 | input_parameters: { 11 | properties: { 12 | interactivity: { type: Schema.slack.types.interactivity }, 13 | channel: { type: Schema.slack.types.channel_id }, 14 | user: { type: Schema.slack.types.user_id }, 15 | }, 16 | required: ["interactivity"], 17 | }, 18 | }); 19 | 20 | workflow.addStep(handleInteractivity, { 21 | interactivity: workflow.inputs.interactivity, 22 | }); 23 | 24 | export default workflow; 25 | -------------------------------------------------------------------------------- /workflows/message_metadata_sender_workflow.ts: -------------------------------------------------------------------------------- 1 | import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; 2 | import { def as sendMessageMetadata } from "../functions/send_metadata_message.ts"; 3 | 4 | /** 5 | * A Workflow is a set of steps that are executed in order. 6 | * Each step in a Workflow is a function. 7 | * https://api.slack.com/future/workflows 8 | */ 9 | const workflow = DefineWorkflow({ 10 | callback_id: "message-metadata-sender-workflow", 11 | title: "Message Metadata Sender Workflow", 12 | input_parameters: { 13 | properties: { 14 | channelId: { type: Schema.slack.types.channel_id }, 15 | }, 16 | required: ["channelId"], 17 | }, 18 | }); 19 | 20 | workflow.addStep(sendMessageMetadata, { 21 | channelId: workflow.inputs.channelId, 22 | }); 23 | 24 | export default workflow; 25 | -------------------------------------------------------------------------------- /triggers/events/app_mentioned.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import workflowDef from "../../workflows/channel_event_workflow.ts"; 3 | 4 | /** 5 | * See https://api.slack.com/future/triggers/event 6 | * required scopes: reactions:read 7 | */ 8 | const trigger: Trigger = { 9 | type: "event", 10 | name: "app_mentioned event trigger", 11 | workflow: `#/workflows/${workflowDef.definition.callback_id}`, 12 | event: { 13 | event_type: "slack#/events/app_mentioned", 14 | // TODO: Listing all the channels to enable here is required 15 | channel_ids: ["CLT1F93TP"], 16 | }, 17 | inputs: { 18 | channelId: { value: "{{data.channel_id}}" }, 19 | messageTs: { value: "{{data.message_ts}}" }, 20 | userId: { value: "{{data.user_id}}" }, 21 | }, 22 | }; 23 | 24 | export default trigger; 25 | -------------------------------------------------------------------------------- /workflows/reaction_added_event_setup_workflow.ts: -------------------------------------------------------------------------------- 1 | import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; 2 | import { def as manageTriggers } from "../functions/manage_reaction_added_event_trigger.ts"; 3 | import { default as eventWorkflowDef } from "./reaction_added_event_workflow.ts"; 4 | 5 | /** 6 | * https://api.slack.com/future/workflows 7 | */ 8 | const workflow = DefineWorkflow({ 9 | callback_id: "reaction-added-event-setup-workflow", 10 | title: "The reaction_added Event Workflow Configurator", 11 | input_parameters: { 12 | properties: { 13 | interactivity: { type: Schema.slack.types.interactivity }, 14 | }, 15 | required: ["interactivity"], 16 | }, 17 | }); 18 | 19 | workflow.addStep(manageTriggers, { 20 | interactivity: workflow.inputs.interactivity, 21 | workflowCallbackId: eventWorkflowDef.definition.callback_id, 22 | }); 23 | 24 | export default workflow; 25 | -------------------------------------------------------------------------------- /functions/create_message_template_test.ts: -------------------------------------------------------------------------------- 1 | import * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts"; 2 | import { SlackFunctionTester } from "deno-slack-sdk/mod.ts"; 3 | import { assertEquals } from "https://deno.land/std@0.153.0/testing/asserts.ts"; 4 | import handler from "./create_message_template.ts"; 5 | 6 | // Replaces globalThis.fetch with the mocked copy 7 | mf.install(); 8 | 9 | mf.mock("POST@/api/apps.datastore.put", () => { 10 | return new Response(`{"ok": true, "item": {"id": "111.222"}}`, { 11 | status: 200, 12 | }); 13 | }); 14 | 15 | const { createContext } = SlackFunctionTester("my-function"); 16 | const env = { logLevel: "CRITICAL" }; 17 | 18 | Deno.test("Print all inputs", async () => { 19 | const inputs = { templateName: "name", templateText: "foo" }; 20 | const { outputs } = await handler(createContext({ inputs, env })); 21 | assertEquals(outputs?.templateId, "111.222"); 22 | }); 23 | -------------------------------------------------------------------------------- /workflows/message_metadata_receiver_workflow.ts: -------------------------------------------------------------------------------- 1 | import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; 2 | import { def as printInputs } from "../functions/print_inputs.ts"; 3 | 4 | /** 5 | * A Workflow is a set of steps that are executed in order. 6 | * Each step in a Workflow is a function. 7 | * https://api.slack.com/future/workflows 8 | */ 9 | const workflow = DefineWorkflow({ 10 | callback_id: "message-metadata-receiver-workflow", 11 | title: "Message Metadata Receiver Workflow", 12 | input_parameters: { 13 | properties: { 14 | channelId: { type: Schema.slack.types.channel_id }, 15 | id: { type: Schema.types.string }, 16 | title: { type: Schema.types.string }, 17 | summary: { type: Schema.types.string }, 18 | severity: { type: Schema.types.string }, 19 | }, 20 | required: ["channelId"], 21 | }, 22 | }); 23 | 24 | workflow.addStep(printInputs, { 25 | id: workflow.inputs.id, 26 | }); 27 | 28 | export default workflow; 29 | -------------------------------------------------------------------------------- /triggers/events/reaction_added.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import workflowDef from "../../workflows/channel_event_workflow.ts"; 3 | 4 | /** 5 | * See https://api.slack.com/future/triggers/event 6 | * required scopes: reactions:read 7 | */ 8 | const trigger: Trigger = { 9 | type: "event", 10 | name: "reaction_added event trigger", 11 | workflow: `#/workflows/${workflowDef.definition.callback_id}`, 12 | event: { 13 | event_type: "slack#/events/reaction_added", 14 | // TODO: Listing all the channels to enable here is required 15 | channel_ids: ["CLT1F93TP"], 16 | }, 17 | inputs: { 18 | channelId: { value: "{{data.channel_id}}" }, 19 | messageTs: { value: "{{data.message_ts}}" }, 20 | userId: { value: "{{data.user_id}}" }, 21 | // TODO: You can any of the following and have the same set in the workflow 22 | // reaction: { value: "{{data.reaction}}" }, 23 | }, 24 | }; 25 | 26 | export default trigger; 27 | -------------------------------------------------------------------------------- /functions/print_inputs.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { Logger } from "../utils/logger.ts"; 3 | import { FunctionSourceFile } from "../utils/function_source_file.ts"; 4 | 5 | /** 6 | * See https://api.slack.com/future/functions/custom 7 | */ 8 | export const def = DefineFunction({ 9 | callback_id: "printer", 10 | title: "Print inputs", 11 | source_file: FunctionSourceFile(import.meta.url), 12 | input_parameters: { 13 | properties: { 14 | // You can customize these keys 15 | id: { type: Schema.types.string }, 16 | name: { type: Schema.types.string }, 17 | text: { type: Schema.types.string }, 18 | channel: { type: Schema.slack.types.channel_id }, 19 | ts: { type: Schema.types.string }, 20 | }, 21 | required: [], 22 | }, 23 | output_parameters: { 24 | properties: {}, 25 | required: [], 26 | }, 27 | }); 28 | 29 | export default SlackFunction(def, ({ 30 | inputs, 31 | env, 32 | }) => { 33 | const logger = Logger(env.logLevel); 34 | logger.debug(inputs); 35 | 36 | return { outputs: {} }; 37 | }); 38 | -------------------------------------------------------------------------------- /functions/send_metadata_message_test.ts: -------------------------------------------------------------------------------- 1 | import * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts"; 2 | import { SlackFunctionTester } from "deno-slack-sdk/mod.ts"; 3 | import { assertEquals } from "https://deno.land/std@0.153.0/testing/asserts.ts"; 4 | import handler from "./send_metadata_message.ts"; 5 | 6 | // Replaces globalThis.fetch with the mocked copy 7 | mf.install(); 8 | 9 | mf.mock("POST@/api/chat.postMessage", async (req) => { 10 | const body = await req.formData(); 11 | if (!body.get("metadata")) { 12 | return new Response(`{"ok": false, "error": "metadata is missing!"}`, { 13 | status: 200, 14 | }); 15 | } 16 | return new Response(`{"ok": true, "message": {"ts": "111.222"}}`, { 17 | status: 200, 18 | }); 19 | }); 20 | 21 | const { createContext } = SlackFunctionTester("my-function"); 22 | const env = { logLevel: "CRITICAL" }; 23 | 24 | Deno.test("Send a message with metadata", async () => { 25 | const inputs = { channelId: "C111", messageText: "Hey, how are you doing?" }; 26 | const { outputs } = await handler(createContext({ inputs, env })); 27 | assertEquals(outputs?.messageTs, "111.222"); 28 | }); 29 | -------------------------------------------------------------------------------- /triggers/events/message_metadata_posted.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import workflowDef from "../../workflows/message_metadata_receiver_workflow.ts"; 3 | import IncidentEvent from "../../event_types/incident.ts"; 4 | 5 | /** 6 | * See https://api.slack.com/future/triggers/event 7 | */ 8 | const trigger: Trigger = { 9 | type: "event", 10 | name: "message_metadata_posted event trigger", 11 | workflow: `#/workflows/${workflowDef.definition.callback_id}`, 12 | event: { 13 | event_type: "slack#/events/message_metadata_posted", 14 | metadata_event_type: IncidentEvent.definition.name, 15 | // TODO: Listing all the channels to enable here is required 16 | channel_ids: ["CLT1F93TP"], 17 | }, 18 | inputs: { 19 | channelId: { value: "{{data.channel_id}}" }, 20 | id: { value: "{{data.metadata.event_payload.id}}" }, 21 | title: { value: "{{data.metadata.event_payload.title}}" }, 22 | summary: { value: "{{data.metadata.event_payload.summary}}" }, 23 | severity: { value: "{{data.metadata.event_payload.severity}}" }, 24 | }, 25 | }; 26 | 27 | export default trigger; 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022- Kazuhiro Sera (@seratch) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /functions/build_message_text.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { Logger } from "../utils/logger.ts"; 3 | import { FunctionSourceFile } from "../utils/function_source_file.ts"; 4 | 5 | /** 6 | * See https://api.slack.com/future/functions/custom 7 | */ 8 | export const def = DefineFunction({ 9 | callback_id: "build-message-text", 10 | title: "Build a new message text", 11 | source_file: FunctionSourceFile(import.meta.url), 12 | input_parameters: { 13 | properties: { 14 | messageText: { type: Schema.types.string }, 15 | }, 16 | required: ["messageText"], 17 | }, 18 | output_parameters: { 19 | properties: { 20 | updatedMessageText: { type: Schema.types.string }, 21 | }, 22 | required: ["updatedMessageText"], 23 | }, 24 | }); 25 | 26 | export default SlackFunction(def, ({ 27 | inputs, 28 | env, 29 | }) => { 30 | const logger = Logger(env.logLevel); 31 | logger.debug(inputs); 32 | 33 | const { messageText } = inputs; 34 | const updatedMessageText = 35 | `:wave: You submitted the following message: \n\n>${messageText}`; 36 | return { outputs: { updatedMessageText } }; 37 | }); 38 | -------------------------------------------------------------------------------- /triggers/scheduled/trigger.ts: -------------------------------------------------------------------------------- 1 | import { Trigger } from "deno-slack-api/types.ts"; 2 | import workflowDef from "../../workflows/channel_event_workflow.ts"; 3 | 4 | /** 5 | * See https://api.slack.com/future/triggers/scheduled 6 | */ 7 | const trigger: Trigger = { 8 | type: "scheduled", 9 | name: "Scheduled trigger", 10 | workflow: `#/workflows/${workflowDef.definition.callback_id}`, 11 | inputs: { 12 | // TODO: set a valid channel ID here 13 | channelId: { value: "CLT1F93TP" }, 14 | }, 15 | schedule: { 16 | // TODO: adjust the following start/end_time 17 | start_time: "2022-09-27T10:20:00Z", 18 | end_time: "2037-12-31T23:59:59Z", 19 | // Monthly 20 | // frequency: { 21 | // type: "monthly", 22 | // on_days: ["Monday"], 23 | // on_week_num: 3, 24 | // repeats_every: 1, 25 | // }, 26 | 27 | // Weekly 28 | // frequency: { 29 | // type: "weekly", 30 | // on_days: ["Monday"], 31 | // repeats_every: 1, 32 | // }, 33 | 34 | // Daily 35 | frequency: { 36 | type: "daily", 37 | repeats_every: 1, 38 | }, 39 | }, 40 | }; 41 | 42 | export default trigger; 43 | -------------------------------------------------------------------------------- /functions/verify_channel_id_test.ts: -------------------------------------------------------------------------------- 1 | import * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts"; 2 | import { SlackFunctionTester } from "deno-slack-sdk/mod.ts"; 3 | import { assertEquals } from "https://deno.land/std@0.153.0/testing/asserts.ts"; 4 | import handler from "./verify_channel_id.ts"; 5 | 6 | // Replaces globalThis.fetch with the mocked copy 7 | mf.install(); 8 | 9 | mf.mock("POST@/api/conversations.info", async (req) => { 10 | const body = await req.formData(); 11 | if (body.get("channel")?.toString().startsWith("C")) { 12 | return new Response(`{"ok": true, "channel": {}}`, { 13 | status: 200, 14 | }); 15 | } 16 | return new Response(`{"ok": false, "error": "channel_not_found"}`, { 17 | status: 200, 18 | }); 19 | }); 20 | 21 | const { createContext } = SlackFunctionTester("my-function"); 22 | const env = { logLevel: "CRITICAL" }; 23 | 24 | Deno.test("Verify a valid channel ID", async () => { 25 | const inputs = { channelId: "C12345" }; 26 | const { outputs } = await handler(createContext({ inputs, env })); 27 | assertEquals(outputs?.channelId, inputs.channelId); 28 | }); 29 | 30 | Deno.test("Verify an invalid channel ID", async () => { 31 | const inputs = { channelId: "invalid" }; 32 | const { outputs } = await handler(createContext({ inputs, env })); 33 | assertEquals(outputs?.channelId, undefined); 34 | }); 35 | -------------------------------------------------------------------------------- /functions/send_message_if_any_test.ts: -------------------------------------------------------------------------------- 1 | import * as mf from "https://deno.land/x/mock_fetch@0.3.0/mod.ts"; 2 | import { SlackFunctionTester } from "deno-slack-sdk/mod.ts"; 3 | import { assertEquals } from "https://deno.land/std@0.153.0/testing/asserts.ts"; 4 | import handler from "./send_message_if_any.ts"; 5 | 6 | // Replaces globalThis.fetch with the mocked copy 7 | mf.install(); 8 | 9 | mf.mock("POST@/api/chat.postMessage", () => { 10 | return new Response(`{"ok": true, "message": {"ts": "111.222"}}`, { 11 | status: 200, 12 | }); 13 | }); 14 | 15 | const { createContext } = SlackFunctionTester("my-function"); 16 | const env = { logLevel: "CRITICAL" }; 17 | 18 | Deno.test("Send a message when both channel and message exist", async () => { 19 | const inputs = { channelId: "C111", messageText: "Hey, how are you doing?" }; 20 | const { outputs } = await handler(createContext({ inputs, env })); 21 | assertEquals(outputs?.messageTs, "111.222"); 22 | }); 23 | 24 | Deno.test("Skip sending when channel is missing", async () => { 25 | const inputs = { messageText: "Hey, how are you doing?" }; 26 | const { outputs } = await handler(createContext({ inputs, env })); 27 | assertEquals(outputs?.messageTs, undefined); 28 | }); 29 | 30 | Deno.test("Skip sending when messageText is missing", async () => { 31 | const inputs = { channelId: "C111" }; 32 | const { outputs } = await handler(createContext({ inputs, env })); 33 | assertEquals(outputs?.messageTs, undefined); 34 | }); 35 | -------------------------------------------------------------------------------- /functions/verify_channel_id.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { SlackAPI } from "deno-slack-api/mod.ts"; 3 | import { Logger } from "../utils/logger.ts"; 4 | import { FunctionSourceFile } from "../utils/function_source_file.ts"; 5 | 6 | /** 7 | * See https://api.slack.com/future/functions/custom 8 | */ 9 | export const def = DefineFunction({ 10 | callback_id: "verify-channel-id", 11 | title: "Verify a channel ID", 12 | description: "Verfify a channel ID", 13 | source_file: FunctionSourceFile(import.meta.url), 14 | input_parameters: { 15 | properties: { 16 | channelId: { type: Schema.slack.types.channel_id }, 17 | }, 18 | required: ["channelId"], 19 | }, 20 | output_parameters: { 21 | properties: { 22 | channelId: { type: Schema.slack.types.channel_id }, 23 | }, 24 | required: [], 25 | }, 26 | }); 27 | 28 | export default SlackFunction(def, async ({ 29 | inputs, 30 | env, 31 | token, 32 | }) => { 33 | const logger = Logger(env.logLevel); 34 | logger.debug(inputs); 35 | 36 | const client = SlackAPI(token); 37 | const response = await client.conversations.info({ 38 | channel: inputs.channelId, 39 | }); 40 | if (response.ok) { 41 | return { outputs: { channelId: inputs.channelId } }; 42 | } else { 43 | const error = `Invalid channel ID detected: ${response.error}`; 44 | logger.error(error); 45 | logger.debug(response); 46 | return { outputs: {}, error }; 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /functions/create_message_template.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { SlackAPI } from "deno-slack-api/mod.ts"; 3 | import { Logger } from "../utils/logger.ts"; 4 | import { FunctionSourceFile } from "../utils/function_source_file.ts"; 5 | import { save } from "../datastores/message_templates.ts"; 6 | 7 | /** 8 | * See https://api.slack.com/future/functions/custom 9 | */ 10 | export const def = DefineFunction({ 11 | callback_id: "create-message-template", 12 | title: "Create a new message template", 13 | source_file: FunctionSourceFile(import.meta.url), 14 | input_parameters: { 15 | properties: { 16 | templateName: { type: Schema.types.string }, 17 | templateText: { type: Schema.types.string }, 18 | }, 19 | required: ["templateName", "templateText"], 20 | }, 21 | output_parameters: { 22 | properties: { 23 | templateId: { type: Schema.types.string }, 24 | }, 25 | required: [], 26 | }, 27 | }); 28 | 29 | export default SlackFunction(def, async ({ 30 | inputs, 31 | env, 32 | token, 33 | }) => { 34 | const logger = Logger(env.logLevel); 35 | logger.debug(inputs); 36 | 37 | const client = SlackAPI(token); 38 | const result = await save(client, env, inputs); 39 | if (result.ok) { 40 | return { outputs: { templateId: result.item.id } }; 41 | } else { 42 | const error = `Failed to insert a new record: ${result.error}`; 43 | logger.error(error); 44 | logger.debug(result); 45 | return { outputs: {}, error }; 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /workflows/channel_event_workflow.ts: -------------------------------------------------------------------------------- 1 | import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; 2 | import { def as verifyChannelId } from "../functions/verify_channel_id.ts"; 3 | import { def as sendMessageIfAny } from "../functions/send_message_if_any.ts"; 4 | import { def as handleMessageInteractivity } from "../functions/handle_message_interactivity.ts"; 5 | import { def as printInputs } from "../functions/print_inputs.ts"; 6 | 7 | /** 8 | * A Workflow is a set of steps that are executed in order. 9 | * Each step in a Workflow is a function. 10 | * https://api.slack.com/future/workflows 11 | */ 12 | const workflow = DefineWorkflow({ 13 | callback_id: "channel-event-workflow", 14 | title: "Channel Event Workflow", 15 | input_parameters: { 16 | properties: { 17 | userId: { type: Schema.slack.types.user_id }, 18 | channelId: { type: Schema.slack.types.channel_id }, 19 | messageTs: { type: Schema.types.string }, 20 | }, 21 | required: ["channelId"], 22 | }, 23 | }); 24 | 25 | const verificationStep = workflow.addStep(verifyChannelId, { 26 | channelId: workflow.inputs.channelId, 27 | }); 28 | const sendMessageStep = workflow.addStep(sendMessageIfAny, { 29 | channelId: verificationStep.outputs.channelId, 30 | messageText: "Hi there!", 31 | }); 32 | workflow.addStep(printInputs, { 33 | channel: sendMessageStep.outputs.channelId, 34 | ts: sendMessageStep.outputs.messageTs, 35 | }); 36 | workflow.addStep( 37 | handleMessageInteractivity, 38 | { 39 | channelId: verificationStep.outputs.channelId, 40 | userId: workflow.inputs.userId, 41 | }, 42 | ); 43 | 44 | export default workflow; 45 | -------------------------------------------------------------------------------- /workflows/link_trigger_workflow.ts: -------------------------------------------------------------------------------- 1 | import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; 2 | import { def as buildMessageText } from "../functions/build_message_text.ts"; 3 | 4 | /** 5 | * https://api.slack.com/future/workflows 6 | */ 7 | const workflow = DefineWorkflow({ 8 | callback_id: "link-trigger-workflow", 9 | title: "Simple Link Trigger Workflow", 10 | input_parameters: { 11 | properties: { 12 | interactivity: { type: Schema.slack.types.interactivity }, 13 | channel: { type: Schema.slack.types.channel_id }, 14 | }, 15 | required: ["interactivity"], 16 | }, 17 | }); 18 | 19 | /** 20 | * https://api.slack.com/future/functions#open-a-form 21 | */ 22 | const inputFormStep = workflow.addStep( 23 | Schema.slack.functions.OpenForm, 24 | { 25 | title: "Send message to channel", 26 | interactivity: workflow.inputs.interactivity, 27 | submit_label: "Send message", 28 | fields: { 29 | elements: [{ 30 | name: "channel", 31 | title: "Channel to send message to", 32 | type: Schema.slack.types.channel_id, 33 | default: workflow.inputs.channel, 34 | }, { 35 | name: "messageText", 36 | title: "Message", 37 | type: Schema.types.string, 38 | long: true, 39 | }], 40 | required: ["channel", "messageText"], 41 | }, 42 | }, 43 | ); 44 | 45 | const buildMessageStep = workflow.addStep(buildMessageText, { 46 | messageText: inputFormStep.outputs.fields.messageText, 47 | }); 48 | 49 | workflow.addStep(Schema.slack.functions.SendMessage, { 50 | channel_id: inputFormStep.outputs.fields.channel, 51 | message: buildMessageStep.outputs.updatedMessageText, 52 | }); 53 | 54 | export default workflow; 55 | -------------------------------------------------------------------------------- /functions/send_message_if_any.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { SlackAPI } from "deno-slack-api/mod.ts"; 3 | import { Logger } from "../utils/logger.ts"; 4 | import { FunctionSourceFile } from "../utils/function_source_file.ts"; 5 | 6 | /** 7 | * See https://api.slack.com/future/functions/custom 8 | */ 9 | export const def = DefineFunction({ 10 | callback_id: "send-message-if-any", 11 | title: "Send a message (if any)", 12 | source_file: FunctionSourceFile(import.meta.url), 13 | input_parameters: { 14 | properties: { 15 | channelId: { type: Schema.slack.types.channel_id }, 16 | messageText: { type: Schema.types.string }, 17 | }, 18 | required: [], // nothing is required 19 | }, 20 | output_parameters: { 21 | properties: { 22 | channelId: { type: Schema.slack.types.channel_id }, 23 | messageTs: { type: Schema.types.string }, 24 | }, 25 | required: [], 26 | }, 27 | }); 28 | 29 | export default SlackFunction(def, async ({ 30 | inputs, 31 | env, 32 | token, 33 | }) => { 34 | const logger = Logger(env.logLevel); 35 | logger.debug(inputs); 36 | 37 | const client = SlackAPI(token); 38 | if (inputs.channelId && inputs.messageText) { 39 | const response = await client.chat.postMessage({ 40 | channel: inputs.channelId, 41 | text: inputs.messageText, 42 | }); 43 | if (response.ok) { 44 | return { 45 | outputs: { 46 | channelId: inputs.channelId, 47 | messageTs: response.message.ts, 48 | }, 49 | }; 50 | } else { 51 | const error = `Failed to send a message: ${response.error}`; 52 | logger.error(error); 53 | return { outputs: {}, error }; 54 | } 55 | } 56 | return { outputs: {} }; 57 | }); 58 | -------------------------------------------------------------------------------- /workflows/datastore_workflow.ts: -------------------------------------------------------------------------------- 1 | import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; 2 | import { def as createMessageTemplate } from "../functions/create_message_template.ts"; 3 | import { def as printInputs } from "../functions/print_inputs.ts"; 4 | 5 | /** 6 | * https://api.slack.com/future/workflows 7 | */ 8 | const workflow = DefineWorkflow({ 9 | callback_id: "datastore-workflow", 10 | title: "Datastore Workflow", 11 | input_parameters: { 12 | properties: { 13 | interactivity: { type: Schema.slack.types.interactivity }, 14 | }, 15 | required: ["interactivity"], 16 | }, 17 | }); 18 | 19 | /** 20 | * https://api.slack.com/future/functions#open-a-form 21 | */ 22 | const inputFormStep = workflow.addStep( 23 | Schema.slack.functions.OpenForm, 24 | { 25 | title: "Send message to channel", 26 | interactivity: workflow.inputs.interactivity, 27 | submit_label: "Send message", 28 | fields: { 29 | elements: [{ 30 | name: "templateName", 31 | title: "Template name", 32 | type: Schema.types.string, 33 | default: "My template", 34 | }, { 35 | name: "templateText", 36 | title: "Message text", 37 | type: Schema.types.string, 38 | long: true, 39 | }], 40 | required: ["templateName", "templateText"], 41 | }, 42 | }, 43 | ); 44 | 45 | const createMessageTemplateStep = workflow.addStep(createMessageTemplate, { 46 | templateName: inputFormStep.outputs.fields.templateName, 47 | templateText: inputFormStep.outputs.fields.templateText, 48 | }); 49 | 50 | workflow.addStep(printInputs, { 51 | id: createMessageTemplateStep.outputs.templateId, 52 | name: inputFormStep.outputs.fields.templateName, 53 | text: inputFormStep.outputs.fields.templateText, 54 | }); 55 | 56 | export default workflow; 57 | -------------------------------------------------------------------------------- /workflows/interactive_blocks_workflow.ts: -------------------------------------------------------------------------------- 1 | import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts"; 2 | import { def as handleInteractiveBlocks } from "../functions/handle_interactive_blocks.ts"; 3 | 4 | /** 5 | * https://api.slack.com/future/workflows 6 | */ 7 | const workflow = DefineWorkflow({ 8 | callback_id: "interactive-blocks-workflow", 9 | title: "Interactive Blocks Workflow", 10 | input_parameters: { 11 | properties: { 12 | interactivity: { type: Schema.slack.types.interactivity }, 13 | channel: { type: Schema.slack.types.channel_id }, 14 | user: { type: Schema.slack.types.user_id }, 15 | }, 16 | required: ["interactivity"], 17 | }, 18 | }); 19 | 20 | const sendMessageStep = workflow.addStep(Schema.slack.functions.SendMessage, { 21 | channel_id: workflow.inputs.channel, 22 | message: `Do you approve <@${workflow.inputs.user}>'s time off request?`, 23 | interactive_blocks: [ 24 | { 25 | "type": "actions", 26 | "block_id": "approve-deny-buttons", 27 | "elements": [ 28 | { 29 | type: "button", 30 | text: { 31 | type: "plain_text", 32 | text: "Approve", 33 | }, 34 | action_id: "approve", 35 | style: "primary", 36 | }, 37 | { 38 | type: "button", 39 | text: { 40 | type: "plain_text", 41 | text: "Deny", 42 | }, 43 | action_id: "deny", 44 | style: "danger", 45 | }, 46 | ], 47 | }, 48 | ], 49 | }); 50 | 51 | workflow.addStep(handleInteractiveBlocks, { 52 | action: sendMessageStep.outputs.action, 53 | interactivity: sendMessageStep.outputs.interactivity, 54 | messageLink: sendMessageStep.outputs.message_link, 55 | messageTs: sendMessageStep.outputs.message_ts, 56 | }); 57 | 58 | export default workflow; 59 | -------------------------------------------------------------------------------- /functions/send_metadata_message.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { SlackAPI } from "deno-slack-api/mod.ts"; 3 | import { Logger } from "../utils/logger.ts"; 4 | import { FunctionSourceFile } from "../utils/function_source_file.ts"; 5 | import IncidentEvent from "../event_types/incident.ts"; 6 | 7 | /** 8 | * See https://api.slack.com/future/functions/custom 9 | */ 10 | export const def = DefineFunction({ 11 | callback_id: "send-metadata-message", 12 | title: "Send a message with its metadata", 13 | source_file: FunctionSourceFile(import.meta.url), 14 | input_parameters: { 15 | properties: { 16 | channelId: { type: Schema.slack.types.channel_id }, 17 | }, 18 | required: ["channelId"], 19 | }, 20 | output_parameters: { 21 | properties: { 22 | channelId: { type: Schema.slack.types.channel_id }, 23 | messageTs: { type: Schema.types.string }, 24 | }, 25 | required: [], 26 | }, 27 | }); 28 | 29 | export default SlackFunction(def, async ({ 30 | inputs, 31 | env, 32 | token, 33 | }) => { 34 | const logger = Logger(env.logLevel); 35 | logger.debug(inputs); 36 | 37 | const client = SlackAPI(token); 38 | const response = await client.chat.postMessage({ 39 | channel: inputs.channelId, 40 | text: "We have an incident!", 41 | metadata: { 42 | event_type: IncidentEvent, 43 | event_payload: { 44 | id: crypto.randomUUID(), 45 | title: "A critical incident", 46 | summary: "Something wrong is happening!", 47 | severity: "Critical", 48 | }, 49 | }, 50 | }); 51 | if (response.ok) { 52 | return { 53 | outputs: { 54 | channelId: inputs.channelId, 55 | messageTs: response.message.ts, 56 | }, 57 | }; 58 | } else { 59 | const error = `Failed to send a message: ${response.error}`; 60 | logger.error(error); 61 | return { outputs: {}, error }; 62 | } 63 | }); 64 | -------------------------------------------------------------------------------- /functions/modals.ts: -------------------------------------------------------------------------------- 1 | export const buildNewModalView = function () { 2 | return { 3 | "type": "modal", 4 | "callback_id": "deny-reason-submission", 5 | "title": { 6 | "type": "plain_text", 7 | "text": "Reason for the denial", 8 | }, 9 | "notify_on_close": true, 10 | "submit": { 11 | "type": "plain_text", 12 | "text": "Confirm", 13 | }, 14 | "blocks": [ 15 | { 16 | "type": "input", 17 | "block_id": crypto.randomUUID(), 18 | "element": { 19 | "type": "plain_text_input", 20 | "action_id": "deny-reason", 21 | "multiline": true, 22 | "initial_value": "", 23 | "placeholder": { 24 | "type": "plain_text", 25 | "text": "Share the reason why you denied the request in detail", 26 | }, 27 | }, 28 | "label": { 29 | "type": "plain_text", 30 | "text": "Reason", 31 | }, 32 | }, 33 | { 34 | "type": "actions", 35 | "block_id": "clear", 36 | "elements": [ 37 | { 38 | type: "button", 39 | text: { 40 | type: "plain_text", 41 | text: "Clear all the inputs", 42 | }, 43 | action_id: "clear-inputs", 44 | style: "danger", 45 | }, 46 | ], 47 | }, 48 | ], 49 | }; 50 | }; 51 | 52 | export const buildConfirmationView = function (reason: string) { 53 | return { 54 | "type": "modal", 55 | "callback_id": "deny-reason-confirmation", 56 | "title": { 57 | "type": "plain_text", 58 | "text": "Reason for the denial", 59 | }, 60 | "notify_on_close": true, 61 | "submit": { 62 | "type": "plain_text", 63 | "text": "Submit", 64 | }, 65 | "private_metadata": reason, 66 | "blocks": [ 67 | { 68 | "type": "section", 69 | "text": { 70 | "type": "mrkdwn", 71 | "text": reason, 72 | }, 73 | }, 74 | ], 75 | }; 76 | }; 77 | -------------------------------------------------------------------------------- /datastores/message_templates.ts: -------------------------------------------------------------------------------- 1 | import { DefineDatastore, Schema } from "deno-slack-sdk/mod.ts"; 2 | import { Env } from "deno-slack-sdk/types.ts"; 3 | import { SlackAPIClient } from "deno-slack-api/types.ts"; 4 | import { Logger } from "../utils/logger.ts"; 5 | 6 | export const DATASTORE_NAME = "message_templates"; 7 | 8 | // requires "datastore:read", "datastore:write" 9 | export const def = DefineDatastore({ 10 | name: DATASTORE_NAME, 11 | primary_key: "id", 12 | attributes: { 13 | id: { 14 | type: Schema.types.string, 15 | required: true, 16 | }, 17 | name: { 18 | type: Schema.types.string, 19 | required: true, 20 | }, 21 | text: { 22 | type: Schema.types.string, 23 | required: true, 24 | }, 25 | }, 26 | }); 27 | 28 | export interface SaveArgs { 29 | id?: string; 30 | templateName: string; 31 | templateText: string; 32 | } 33 | 34 | export async function save( 35 | client: SlackAPIClient, 36 | env: Env, 37 | args: SaveArgs, 38 | ) { 39 | const logger = Logger(env.logLevel); 40 | logger.debug(`Saving a recored: ${JSON.stringify(args)}`); 41 | const result = await client.apps.datastore.put({ 42 | datastore: DATASTORE_NAME, 43 | item: { 44 | id: args.id ?? crypto.randomUUID(), 45 | name: args.templateName, 46 | message: args.templateText, 47 | }, 48 | }); 49 | logger.debug(`Save result: ${JSON.stringify(result)}`); 50 | return result; 51 | } 52 | 53 | export async function findById( 54 | client: SlackAPIClient, 55 | env: Env, 56 | id: string, 57 | ) { 58 | const logger = Logger(env.logLevel); 59 | logger.debug(`Finding a record for id: ${id}`); 60 | const result = await client.apps.datastore.get({ 61 | datastore: DATASTORE_NAME, 62 | item: { id }, 63 | }); 64 | logger.debug(`Found: ${JSON.stringify(result)}`); 65 | return result; 66 | } 67 | 68 | export async function deleteById( 69 | client: SlackAPIClient, 70 | env: Env, 71 | id: string, 72 | ) { 73 | const logger = Logger(env.logLevel); 74 | logger.debug(`Deleting a record for id: ${id}`); 75 | const result = await client.apps.datastore.delete({ 76 | datastore: DATASTORE_NAME, 77 | item: { id }, 78 | }); 79 | logger.debug(`Deletion result: ${JSON.stringify(result)}`); 80 | return result; 81 | } 82 | -------------------------------------------------------------------------------- /manifest.ts: -------------------------------------------------------------------------------- 1 | import { Manifest } from "deno-slack-sdk/mod.ts"; 2 | import IncidentEvent from "./event_types/incident.ts"; 3 | import { def as messageTemplates } from "./datastores/message_templates.ts"; 4 | import interactivityWorkflow from "./workflows/interactivity_workflow.ts"; 5 | import interactiveBlocksWorkflow from "./workflows/interactive_blocks_workflow.ts"; 6 | import channelEventWorkflow from "./workflows/channel_event_workflow.ts"; 7 | import linkTriggerWorkflow from "./workflows/link_trigger_workflow.ts"; 8 | import datastoreWorkflow from "./workflows/datastore_workflow.ts"; 9 | import messageMetadataSenderWorkflow from "./workflows/message_metadata_sender_workflow.ts"; 10 | import messageMetadataReceiverWorkflow from "./workflows/message_metadata_receiver_workflow.ts"; 11 | import reactionAddedEventSetupWorkflow from "./workflows/reaction_added_event_setup_workflow.ts"; 12 | import reactionAddedEventWorkflow from "./workflows/reaction_added_event_workflow.ts"; 13 | 14 | /** 15 | * See https://api.slack.com/future/manifest 16 | */ 17 | export default Manifest({ 18 | name: "My awesome app", 19 | description: "A template for building Slack apps with Deno", 20 | icon: "assets/icon.png", 21 | workflows: [ 22 | interactivityWorkflow, 23 | interactiveBlocksWorkflow, 24 | channelEventWorkflow, 25 | linkTriggerWorkflow, 26 | datastoreWorkflow, 27 | messageMetadataSenderWorkflow, 28 | messageMetadataReceiverWorkflow, 29 | reactionAddedEventSetupWorkflow, 30 | reactionAddedEventWorkflow, 31 | ], 32 | events: [IncidentEvent], 33 | datastores: [messageTemplates], 34 | outgoingDomains: [], 35 | botScopes: [ 36 | "commands", 37 | "chat:write", 38 | "chat:write.public", 39 | // for triggers/channel_events/app_mentioned.ts 40 | "app_mentions:read", 41 | // for triggers/channel_events/reaction_added.ts 42 | "reactions:read", 43 | // for triggers/events/message_metadata_posted.ts 44 | "metadata.message:read", 45 | // for datastores/message_templates.ts 46 | "datastore:read", 47 | "datastore:write", 48 | // for functions/verify_channel_id.ts 49 | "channels:read", 50 | // for functions/manage_reaction_added_event_trigger.ts 51 | "triggers:read", 52 | "triggers:write", 53 | "channels:join", 54 | ], 55 | }); 56 | -------------------------------------------------------------------------------- /functions/handle_interactive_blocks.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { SlackAPI } from "deno-slack-api/mod.ts"; 3 | import { Logger } from "../utils/logger.ts"; 4 | import { FunctionSourceFile } from "../utils/function_source_file.ts"; 5 | import { buildNewModalView } from "./modals.ts"; 6 | 7 | /** 8 | * See https://api.slack.com/future/functions/custom 9 | */ 10 | export const def = DefineFunction({ 11 | callback_id: "handle-interactive-blocks", 12 | title: "Handle button clicks in interactive_blocks", 13 | source_file: FunctionSourceFile(import.meta.url), 14 | input_parameters: { 15 | properties: { 16 | action: { type: Schema.types.object }, 17 | interactivity: { type: Schema.slack.types.interactivity }, 18 | messageLink: { type: Schema.types.string }, 19 | messageTs: { type: Schema.types.string }, 20 | }, 21 | required: ["action", "interactivity"], 22 | }, 23 | output_parameters: { 24 | properties: {}, 25 | required: [], 26 | }, 27 | }); 28 | 29 | export default SlackFunction(def, async ({ 30 | inputs, 31 | env, 32 | token, 33 | }) => { 34 | const logger = Logger(env.logLevel); 35 | logger.debug(inputs); 36 | 37 | if (inputs.action.action_id === "deny") { 38 | const client = SlackAPI(token); 39 | await client.views.open({ 40 | interactivity_pointer: inputs.interactivity.interactivity_pointer, 41 | view: buildNewModalView(), 42 | }); 43 | return { 44 | completed: false, 45 | }; 46 | } 47 | return { 48 | completed: true, 49 | outputs: {}, 50 | }; 51 | }) 52 | .addBlockActionsHandler( 53 | "clear-inputs", 54 | async ({ body, env, token }) => { 55 | const logger = Logger(env.logLevel); 56 | logger.debug(JSON.stringify(body, null, 2)); 57 | const client = SlackAPI(token); 58 | await client.views.update({ 59 | interactivity_pointer: body.interactivity.interactivity_pointer, 60 | view_id: body.view.id, 61 | view: buildNewModalView(), 62 | }); 63 | return { 64 | completed: false, 65 | }; 66 | }, 67 | ) 68 | .addViewSubmissionHandler( 69 | ["deny-reason-submission"], 70 | ({ view, env }) => { 71 | const logger = Logger(env.logLevel); 72 | const values = view.state.values; 73 | logger.debug(JSON.stringify(values, null, 2)); 74 | const reason = String(Object.values(values)[0]["deny-reason"].value); 75 | if (reason.length <= 5) { 76 | console.log(reason); 77 | const errors: Record = {}; 78 | const blockId = Object.keys(values)[0]; 79 | errors[blockId] = "The reason must be longer than 5 characters"; 80 | return { response_action: "errors", errors }; 81 | } 82 | // nothing to return if you want to close this modal 83 | return; 84 | }, 85 | ) 86 | .addViewClosedHandler( 87 | ["deny-reason-submission", "deny-reason-confirmation"], 88 | ({ view, env }) => { 89 | const logger = Logger(env.logLevel); 90 | logger.debug(JSON.stringify(view, null, 2)); 91 | return; 92 | }, 93 | ); 94 | -------------------------------------------------------------------------------- /functions/handle_interactivity.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { SlackAPI } from "deno-slack-api/mod.ts"; 3 | import { Logger } from "../utils/logger.ts"; 4 | import { FunctionSourceFile } from "../utils/function_source_file.ts"; 5 | import { buildConfirmationView, buildNewModalView } from "./modals.ts"; 6 | 7 | /** 8 | * See https://api.slack.com/future/functions/custom 9 | */ 10 | export const def = DefineFunction({ 11 | callback_id: "handle-interactivity", 12 | title: "Handle all types of interactivity", 13 | source_file: FunctionSourceFile(import.meta.url), 14 | input_parameters: { 15 | properties: { 16 | interactivity: { type: Schema.slack.types.interactivity }, 17 | }, 18 | required: ["interactivity"], 19 | }, 20 | output_parameters: { 21 | properties: {}, 22 | required: [], 23 | }, 24 | }); 25 | 26 | export default SlackFunction(def, async ({ 27 | inputs, 28 | env, 29 | token, 30 | }) => { 31 | const logger = Logger(env.logLevel); 32 | logger.debug(inputs); 33 | 34 | const client = SlackAPI(token); 35 | await client.views.open({ 36 | interactivity_pointer: inputs.interactivity.interactivity_pointer, 37 | view: buildNewModalView(), 38 | }); 39 | return { 40 | completed: false, 41 | }; 42 | }) 43 | .addBlockActionsHandler( 44 | "clear-inputs", 45 | async ({ body, env, token }) => { 46 | const logger = Logger(env.logLevel); 47 | logger.debug(JSON.stringify(body, null, 2)); 48 | const client = SlackAPI(token); 49 | await client.views.update({ 50 | interactivity_pointer: body.interactivity.interactivity_pointer, 51 | view_id: body.view.id, 52 | view: buildNewModalView(), 53 | }); 54 | return { 55 | completed: false, 56 | }; 57 | }, 58 | ) 59 | .addViewSubmissionHandler( 60 | ["deny-reason-submission"], 61 | ({ view, env }) => { 62 | const logger = Logger(env.logLevel); 63 | const values = view.state.values; 64 | logger.debug(JSON.stringify(values, null, 2)); 65 | const reason = String(Object.values(values)[0]["deny-reason"].value); 66 | if (reason.length <= 5) { 67 | console.log(reason); 68 | const errors: Record = {}; 69 | const blockId = Object.keys(values)[0]; 70 | errors[blockId] = "The reason must be longer than 5 characters"; 71 | return { response_action: "errors", errors }; 72 | } 73 | // nothing to return if you want to close this modal 74 | return { response_action: "update", view: buildConfirmationView(reason) }; 75 | }, 76 | ) 77 | .addViewSubmissionHandler( 78 | ["deny-reason-confirmation"], 79 | ({ view, env }) => { 80 | const logger = Logger(env.logLevel); 81 | logger.debug(view.private_metadata); 82 | // nothing to return if you want to close this modal 83 | return; 84 | }, 85 | ) 86 | .addViewClosedHandler( 87 | ["deny-reason-submission", "deny-reason-confirmation"], 88 | ({ view, env }) => { 89 | const logger = Logger(env.logLevel); 90 | logger.debug(JSON.stringify(view, null, 2)); 91 | return; 92 | }, 93 | ); 94 | -------------------------------------------------------------------------------- /functions/handle_message_interactivity.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { SlackAPI } from "deno-slack-api/mod.ts"; 3 | import { Logger } from "../utils/logger.ts"; 4 | import { FunctionSourceFile } from "../utils/function_source_file.ts"; 5 | 6 | /** 7 | * See https://api.slack.com/future/functions/custom 8 | */ 9 | export const def = DefineFunction({ 10 | callback_id: "handle-message-interactivity", 11 | title: "Handle interactivity within a channel message", 12 | source_file: FunctionSourceFile(import.meta.url), 13 | input_parameters: { 14 | properties: { 15 | channelId: { type: Schema.slack.types.channel_id }, 16 | userId: { type: Schema.slack.types.user_id }, 17 | }, 18 | required: ["channelId", "userId"], 19 | }, 20 | output_parameters: { 21 | properties: {}, 22 | required: [], 23 | }, 24 | }); 25 | 26 | export default SlackFunction(def, async ({ 27 | inputs, 28 | env, 29 | token, 30 | }) => { 31 | const logger = Logger(env.logLevel); 32 | logger.debug(inputs); 33 | 34 | if (inputs.channelId === undefined) { 35 | return { outputs: {} }; 36 | } 37 | 38 | const client = SlackAPI(token); 39 | // Note that chat.postEphemeral with buttons does not work as of Sep 2022 40 | await client.chat.postMessage({ 41 | channel: inputs.channelId, 42 | text: "Hi there! How many functions have you created?", 43 | blocks: [ 44 | { 45 | "type": "section", 46 | "text": { 47 | "type": "mrkdwn", 48 | "text": "Hi there! How many functions have you created?", 49 | }, 50 | }, 51 | { 52 | "type": "actions", 53 | "block_id": "buttons", 54 | "elements": [ 55 | { 56 | type: "button", 57 | text: { 58 | type: "plain_text", 59 | text: "1", 60 | }, 61 | action_id: "button-1", 62 | }, 63 | { 64 | type: "button", 65 | text: { 66 | type: "plain_text", 67 | text: "2", 68 | }, 69 | action_id: "button-2", 70 | }, 71 | { 72 | type: "button", 73 | text: { 74 | type: "plain_text", 75 | text: "3", 76 | }, 77 | action_id: "button-3", 78 | }, 79 | { 80 | type: "button", 81 | text: { 82 | type: "plain_text", 83 | text: "More than 3!", 84 | }, 85 | action_id: "button-more", 86 | }, 87 | ], 88 | }, 89 | ], 90 | }); 91 | return { 92 | completed: false, 93 | }; 94 | }) 95 | .addBlockActionsHandler( 96 | ["button-1", "button-2", "button-3", "button-more"], 97 | async ({ body, action, env, token }) => { 98 | const logger = Logger(env.logLevel); 99 | logger.debug(JSON.stringify(body, null, 2)); 100 | 101 | const client = SlackAPI(token); 102 | await client.views.open({ 103 | interactivity_pointer: body.interactivity.interactivity_pointer, 104 | view: { 105 | "type": "modal", 106 | "title": { 107 | "type": "plain_text", 108 | "text": "Clicked!", 109 | }, 110 | "blocks": [ 111 | { 112 | "type": "section", 113 | "text": { 114 | "type": "mrkdwn", 115 | "text": `You clicked *${action.text.text}*!`, 116 | }, 117 | }, 118 | ], 119 | }, 120 | }); 121 | return { completed: true }; 122 | }, 123 | ); 124 | -------------------------------------------------------------------------------- /functions/manage_reaction_added_event_trigger.ts: -------------------------------------------------------------------------------- 1 | import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts"; 2 | import { SlackAPI } from "deno-slack-api/mod.ts"; 3 | import { Logger } from "../utils/logger.ts"; 4 | import { FunctionSourceFile } from "../utils/function_source_file.ts"; 5 | 6 | /** 7 | * See https://api.slack.com/future/functions/custom 8 | */ 9 | export const def = DefineFunction({ 10 | callback_id: "manage-reaction-added-event-trigger", 11 | title: "Manage a reaction_added event trigger", 12 | source_file: FunctionSourceFile(import.meta.url), 13 | input_parameters: { 14 | properties: { 15 | interactivity: { type: Schema.slack.types.interactivity }, 16 | workflowCallbackId: { type: Schema.types.string }, 17 | }, 18 | required: ["interactivity"], 19 | }, 20 | output_parameters: { 21 | properties: {}, 22 | required: [], 23 | }, 24 | }); 25 | 26 | export default SlackFunction(def, async ({ 27 | inputs, 28 | env, 29 | token, 30 | }) => { 31 | const logger = Logger(env.logLevel); 32 | logger.debug(inputs); 33 | 34 | const client = SlackAPI(token); 35 | const allTriggers = await client.workflows.triggers.list({}); 36 | let triggerToUpdate = undefined; 37 | // find the trigger to update 38 | if (allTriggers.triggers) { 39 | for (const trigger of allTriggers.triggers) { 40 | if ( 41 | trigger.workflow.callback_id === inputs.workflowCallbackId && 42 | trigger.event_type === "slack#/events/reaction_added" 43 | ) { 44 | triggerToUpdate = trigger; 45 | } 46 | } 47 | } 48 | const channelIds = triggerToUpdate?.channel_ids != undefined 49 | ? triggerToUpdate.channel_ids 50 | : []; 51 | await client.views.open({ 52 | interactivity_pointer: inputs.interactivity.interactivity_pointer, 53 | view: { 54 | "type": "modal", 55 | "callback_id": "configure-workflow", 56 | "title": { 57 | "type": "plain_text", 58 | "text": "Workflow Configuration", 59 | }, 60 | "notify_on_close": true, 61 | "submit": { 62 | "type": "plain_text", 63 | "text": "Confirm", 64 | }, 65 | "blocks": [ 66 | { 67 | "type": "input", 68 | "block_id": "block", 69 | "element": { 70 | "type": "multi_channels_select", 71 | "placeholder": { 72 | "type": "plain_text", 73 | "text": "Select channels to add", 74 | }, 75 | "initial_channels": channelIds, 76 | "action_id": "channels", 77 | }, 78 | "label": { 79 | "type": "plain_text", 80 | "text": "Channels to enable the workflow", 81 | }, 82 | }, 83 | ], 84 | }, 85 | }); 86 | return { 87 | completed: false, 88 | }; 89 | }) 90 | .addViewSubmissionHandler( 91 | ["configure-workflow"], 92 | async ({ view, inputs, env, token }) => { 93 | const logger = Logger(env.logLevel); 94 | const { workflowCallbackId } = inputs; 95 | const channelIds = view.state.values.block.channels.selected_channels; 96 | const triggerInputs = { 97 | channelId: { 98 | value: "{{data.channel_id}}", 99 | }, 100 | messageTs: { 101 | value: "{{data.message_ts}}", 102 | }, 103 | reaction: { 104 | value: "{{data.reaction}}", 105 | }, 106 | }; 107 | 108 | const client = SlackAPI(token); 109 | const authTest = await client.auth.test({}); 110 | logger.info(authTest); 111 | 112 | const allTriggers = await client.workflows.triggers.list({}); 113 | let modalMessage = "The configuration is done!"; 114 | try { 115 | let triggerToUpdate = undefined; 116 | // find the trigger to update 117 | if (allTriggers.triggers) { 118 | for (const trigger of allTriggers.triggers) { 119 | if ( 120 | trigger.workflow.callback_id === workflowCallbackId && 121 | trigger.event_type === "slack#/events/reaction_added" 122 | ) { 123 | triggerToUpdate = trigger; 124 | } 125 | } 126 | } 127 | logger.info(triggerToUpdate); 128 | 129 | if (triggerToUpdate === undefined) { 130 | const creation = await client.workflows.triggers.create({ 131 | type: "event", 132 | name: "reaction_added event trigger", 133 | workflow: `#/workflows/${workflowCallbackId}`, 134 | event: { 135 | event_type: "slack#/events/reaction_added", 136 | channel_ids: channelIds, 137 | }, 138 | inputs: triggerInputs, 139 | }); 140 | logger.info(`A new trigger created: ${JSON.stringify(creation)}`); 141 | } else { 142 | const update = await client.workflows.triggers.update({ 143 | trigger_id: triggerToUpdate.id, 144 | type: "event", 145 | name: "reaction_added event trigger", 146 | workflow: `#/workflows/${workflowCallbackId}`, 147 | event: { 148 | event_type: "slack#/events/reaction_added", 149 | channel_ids: channelIds, 150 | }, 151 | inputs: triggerInputs, 152 | }); 153 | logger.info(`A new trigger updated: ${JSON.stringify(update)}`); 154 | } 155 | for (const channelId of channelIds) { 156 | const joinResult = await client.conversations.join({ 157 | channel: channelId, 158 | }); 159 | logger.debug(joinResult); 160 | } 161 | } catch (e) { 162 | logger.error(e); 163 | modalMessage = e; 164 | } 165 | // nothing to return if you want to close this modal 166 | return { 167 | response_action: "update", 168 | view: { 169 | "type": "modal", 170 | "callback_id": "configure-workflow", 171 | "notify_on_close": true, 172 | "title": { 173 | "type": "plain_text", 174 | "text": "Workflow Configuration", 175 | }, 176 | "blocks": [ 177 | { 178 | "type": "section", 179 | "text": { 180 | "type": "mrkdwn", 181 | "text": modalMessage, 182 | }, 183 | }, 184 | ], 185 | }, 186 | }; 187 | }, 188 | ) 189 | .addViewClosedHandler( 190 | ["configure-workflow"], 191 | ({ view, env }) => { 192 | const logger = Logger(env.logLevel); 193 | logger.debug(JSON.stringify(view, null, 2)); 194 | return { 195 | outputs: {}, 196 | completed: true, 197 | }; 198 | }, 199 | ); 200 | --------------------------------------------------------------------------------