Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
December 29, 2022 02:15 pm GMT

Slack Next-gen Platform - Trigger Configurator

In this tutorial, you'll learn how to build [a workflow to configure another workflow's triggers in your Slack's next-generation platform apps.

When you learned how to add an event trigger that requires channel_ids in its definition (If you haven't read the article, reading it first is highly recommended), I've mentioned that hard-coding the list in code could be a difficulty for many use cases.

This tutorial provides a solution for it. When you have a workflow ("Main" workflow) with an event trigger, in the same app, you can have another workflow ("Configurator" workflow), which configures the trigger for the "Main" workflow. You may wonder if this can be complex, but the implementation is relatively simple. Let's get started!

Prerequisites

If you're new to the platform, please read my The Simplest "Hello World" tutorial first. In a nutshell, you'll need a paid Slack workspace, and permission to use the beta feature in the workspace. And then, you can connect your Slack CLI with the workspace.

If all the above are already done, you're ready to build your first app. Let's get started!

Create a Blank Project

When you start a new project, you can run slack create command. In this tutorial, you will build an app from scratch. So select "Blank project" from the list:

$ slack create? Select a template to build from:  Hello World  A simple workflow that sends a greeting  Scaffolded project  A solid foundational project that uses a Slack datastore> Blank project  A, well.. blank project  To see all available samples, visit github.com/slack-samples.

Once the project is generated, let's check if slack run command works without any issues. This command installs a "dev" version of your new app into your connected Slack workspace. Now your app's bot user is in the workspace, and your app has its bot token for API calls.

$ cd vibrant-orca-513$ slack run? Choose a workspace  seratch  T03E94MJU   App is not installed to this workspaceUpdating dev app install for workspace "Acme Corp"  Outgoing domains   No allowed outgoing domains are configured   If your function makes network requests, you will need to allow the outgoing domains   Learn more about upcoming changes to outgoing domains: https://api.slack.com/future/changelog  seratch of Acme CorpConnected, awaiting events

If you see Connected, awaiting events log message, the app is successfully connected to Slack. You can hit "Ctrl + C" to terminate the local app process.

In this tutorial, we'll build three workflows:

  • The "Main" workflow that needs a "reaction_added" event trigger
  • The "Configurator" workflow that configures the "Main" workflow's trigger via a webhook request
  • The "Configurator" workflow that configures the "Main" workflow's trigger using user inputs sent from a modal dialog

Add The "Main" Workflow

First off, create a new file named main_workflow.ts with the following content:

import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";export const workflow = DefineWorkflow({  callback_id: "main_event_workflow",  title: "Main event workflow",  input_parameters: {    properties: {      // The list of input properties needs to be consistent with      // the trigger operation code in `manage_triggers.ts`      userId: { type: Schema.slack.types.user_id },      channelId: { type: Schema.slack.types.channel_id },      messageTs: { type: Schema.types.string },      reaction: { type: Schema.types.string },    },    required: ["userId", "channelId", "reaction"],  },});// Send an ephemeral message when an expected reaction is addedworkflow.addStep(Schema.slack.functions.SendEphemeralMessage, {  user_id: workflow.inputs.userId,  channel_id: workflow.inputs.channelId,  message: `Thanks for adding :${workflow.inputs.reaction}:!`,});// This source file does not have its trigger definition!// The "reaction_added" event trigger will be created by// the "Configurator" workflow's custom function.

As mentioned at the bottom of the source code, you don't have any trigger definition in this file, as you'll generate a trigger using another workflow.

Also, before forgetting, let's add the workflow to manifest.ts:

import { Manifest } from "deno-slack-sdk/mod.ts";// Add thisimport { workflow as MainWorkflow } from "./main_workflow.ts";export default Manifest({  name: "vibrant-orca-513",  description: "Configurator Demo",  icon: "assets/default_new_app_icon.png",  workflows: [MainWorkflow], // Add this  outgoingDomains: [],  botScopes: [    "commands",    "chat:write",    "chat:write.public",    // We're going to use "reaction_added" event trigger    "reactions:read",  ],});

Add The Custom Function To Configure Triggers

The next step is to add your custom function that generates a trigger for a different workflow. Note that the "Configurator" workflow itself will be invoked via a trigger that is created using a source code file.

In this part, you will create three source files - manage_triggers.ts, join_channels.ts, and configure.ts.

Let's start with manage_triggers.ts, which provides the core logic for trigger management. Save the following code as manage_triggers.ts:

import { SlackAPIClient } from "deno-slack-api/types.ts";// These need to be consistent with `main_workflow.ts`const triggerEventType = "slack#/events/reaction_added";const triggerName = "reaction_added event trigger";const triggerInputs = {  userId: { value: "{{data.user_id}}" },  channelId: { value: "{{data.channel_id}}" },  messageTs: { value: "{{data.message_ts}}" },  reaction: { value: "{{data.reaction}}" },};/** * Check if the target trigger already exists and then return the metadata if it exists. * @param client Slack API client * @param workflowCallbackId the target workflow's callback_id * @returns the existing trigger's metadata (can be undefined) */export async function findTriggerToUpdate(  client: SlackAPIClient,  workflowCallbackId: string,): Promise<Record<string, string> | undefined> {  // Fetch all the triggers that this app can access  const listResponse = await client.workflows.triggers.list({ is_owner: true });  // Find the target trigger in the list  // If the list contains duplicated items, this function may not work properly  if (listResponse && listResponse.triggers) {    for (const trigger of listResponse.triggers) {      if (        trigger.workflow.callback_id === workflowCallbackId &&        trigger.event_type === triggerEventType      ) {        return trigger;      }    }  }  // The target trigger does not exist yet  return undefined;}/** * Create or update the target trigger. The operation by this method is not atomic, meaning that duplicated triggers can be generated when the creation is requested simultaneously. Also, when updating the trigger, there is no validation of conflicts. * @param client Slack API client * @param workflowCallbackId the target workflow's callback_id * @param channelIds the list of channel IDs to enable the trigger * @param triggerId the existing trigger ID (only for updates) */export async function createOrUpdateTrigger(  client: SlackAPIClient,  workflowCallbackId: string,  channelIds: string[],  triggerId?: string,): Promise<void> {  // Since the Deno SDK type constraints require hard-coding the list of string items, there is no way to pass the method argument for it directly. Thus, we have to bypass the type check here.  // deno-lint-ignore no-explicit-any  const channel_ids = channelIds as any;  if (triggerId) {    // Update the existing trigger    const update = await client.workflows.triggers.update({      trigger_id: triggerId,      type: "event",      name: triggerName,      workflow: `#/workflows/${workflowCallbackId}`,      event: { event_type: triggerEventType, channel_ids },      inputs: triggerInputs,    });    if (update.error) {      const error = `Failed to update a trigger! - ${JSON.stringify(update)}`;      throw new Error(error);    }    console.log(`The trigger updated: ${JSON.stringify(update)}`);  } else {    // Create a new trigger    const creation = await client.workflows.triggers.create({      type: "event",      name: triggerName,      workflow: `#/workflows/${workflowCallbackId}`,      event: { event_type: triggerEventType, channel_ids },      inputs: triggerInputs,    });    if (creation.error) {      const error = `Failed to create a trigger! - ${JSON.stringify(creation)}`;      throw new Error(error);    }    console.log(`A new trigger created: ${JSON.stringify(creation)}`);  }}

The next one is join_channels.ts, which provides a utility method to join a lot of channels easily:

import { SlackAPIClient } from "deno-slack-api/types.ts";/** * Join all the channels. * Even when the app's bot user has joined, the API does not return an error. * @param client * @param channelIds * @returns */export async function joinAllChannels(  client: SlackAPIClient,  channelIds: string[],): Promise<string | undefined> {  const futures = channelIds.map((c) => _joinChannel(client, c));  const results = (await Promise.all(futures)).filter((r) => r !== undefined);  if (results.length > 0) {    throw new Error(results[0]);  }  return undefined;}async function _joinChannel(  client: SlackAPIClient,  channelId: string,): Promise<string | undefined> {  const response = await client.conversations.join({ channel: channelId });  if (response.error) {    const error = `Failed to join <#${channelId}> due to ${response.error}`;    console.log(error);    return error;  }}

The reason to add your app's bot user is that, without a channel membership, your app cannot call many common APIs such as conversations.history, conversations.replies, and reactions.get APIs. To build something meaningful with channel events, these APIs will be used a lot.

Lastly, let's add configure.ts, a custom function for your "Configurator" workflow. This code has references to the above two source files:

import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import {  createOrUpdateTrigger,  findTriggerToUpdate,} from "./manage_triggers.ts";import { joinAllChannels } from "./join_channels.ts";import { FunctionSourceFile } from "https://deno.land/x/[email protected]/mod.ts";export const def = DefineFunction({  callback_id: "configure",  title: "Configure a trigger",  source_file: FunctionSourceFile(import.meta.url),  input_parameters: {    properties: {      workflowCallbackId: { type: Schema.types.string },      channelIds: {        type: Schema.types.array,        items: { type: Schema.slack.types.channel_id },      },    },    required: ["workflowCallbackId", "channelIds"],  },  output_parameters: { properties: {}, required: [] },});export default SlackFunction(def, async ({ inputs, client }) => {  try {    await createOrUpdateTrigger(      client,      inputs.workflowCallbackId,      inputs.channelIds,      (await findTriggerToUpdate(client, inputs.workflowCallbackId))?.id,    );  } catch (e) {    const error = `Failed to create/update a trigger due to ${e}.`;    return { error };  }  // If you don't need to invite your app's bot user to the channels,  // you can safely remove the following part:  const failure = await joinAllChannels(client, inputs.channelIds);  if (failure) {    const error = `Failed to join channels due to ${failure}.`;    return { error };  }  return { outputs: {} };});

Create The "Configurator" Workflow (Webhook Trigger)

Let's create your "Configurator" workflow that uses configure.ts for trigger management.

Create a new file named webhook_configurator.ts:

import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";export const workflow = DefineWorkflow({  callback_id: "webhook_configurator",  title: "Webhook Configurator",  input_parameters: {    properties: {      channel_ids: {        type: Schema.types.array,        items: { type: Schema.slack.types.channel_id },      },    },    required: ["channel_ids"],  },});import { def as Configure } from "./configure.ts";import { workflow as MainWorkflow } from "./main_workflow.ts";workflow.addStep(Configure, {  // The callback_id here must be the one for "Main" workflow!  workflowCallbackId: MainWorkflow.definition.callback_id,  channelIds: workflow.inputs.channel_ids,});// Trigger to invoke the "Configurator" workflowimport { Trigger } from "deno-slack-api/types.ts";const trigger: Trigger<typeof workflow.definition> = {  type: "webhook",  name: "Webhook Configurator Trigger",  // The callback_id here must be the one for "Configurator" workflow!  workflow: `#/workflows/${workflow.definition.callback_id}`,  inputs: { "channel_ids": { "value": "{{data.channel_ids}}" } },};export default trigger;

Don't forget to add this workflow to manifest.ts. Adding the "Configurator" workflow to the workflows list, plus a few scopes need to be added to botScopes.

import { Manifest } from "deno-slack-sdk/mod.ts";import { workflow as MainWorkflow } from "./main_workflow.ts";// Add thisimport { workflow as ConfiguratorWorkflow } from "./webhook_configurator.ts";export default Manifest({  name: "vibrant-orca-513",  description: "Configurator Demo",  icon: "assets/default_new_app_icon.png",  workflows: [MainWorkflow, ConfiguratorWorkflow], // Add this  outgoingDomains: [],  botScopes: [    "commands",    "chat:write",    "chat:write.public",    // We're going to use "reaction_added" event trigger    "reactions:read",    // Required for `configure.ts`    "triggers:read",    "triggers:write",    "channels:join",  ],});

Run slack triggers create --trigger-def ./webhook_configurator.ts to generate a webhook trigger:

$ slack triggers create --trigger-def ./webhook_configurator.ts? Choose an app  seratch (dev)  T03E94MJU   vibrant-orca-513 (dev) A04FRL4323G Trigger created   Trigger ID:   Ft04FY7H1AM9   Trigger Type: webhook   Trigger Name: Webhook Configurator Trigger   Webhook URL:  https://hooks.slack.com/triggers/T03E94***/***/***

To invoke the "Configurator" workflow, send an HTTP POST request to the webhook URL with {"channel_ids": ["C03E94MKS"]} in its request body. To know the channel ID string values, go through these steps in the Slack client UI.

$ curl -XPOST \  https://hooks.slack.com/triggers/T03E94***/***/*** \  -d'{"channel_ids": ["C03E94MKS"]}'{"ok":true}%

Check the slack run command terminal. If you don't see any errors, it should be successful!

$ slack run? Choose a workspace  seratch  T03E94MJU   vibrant-orca-513 A04FRL4323GUpdating dev app install for workspace "Acme Corp"  Outgoing domains   No allowed outgoing domains are configured   If your function makes network requests, you will need to allow the outgoing domains   Learn more about upcoming changes to outgoing domains: https://api.slack.com/future/changelog  seratch of Acme CorpConnected, awaiting events2022-12-20 14:14:29 [info] [Fn04GN1S5CP2] (Trace=Tr04FRNYD1QW) Function execution started for workflow function 'Webhook Configurator'2022-12-20 14:14:29 [info] [Wf04FY7BP8M9] (Trace=Tr04FHQLH3K9) Execution started for workflow 'Webhook Configurator'2022-12-20 14:14:30 [info] [Wf04FY7BP8M9] (Trace=Tr04FHQLH3K9) Executing workflow step 1 of 12022-12-20 14:14:30 [info] [Fn04FVCA06N9] (Trace=Tr04FHQLH3K9) Function execution started for app function 'Configure a trigger'A new trigger created: {"ok":true,"trigger":{ ... }}2022-12-20 14:14:32 [info] [Fn04FVCA06N9] (Trace=Tr04FHQLH3K9) Function execution completed for function 'Configure a trigger'2022-12-20 14:14:32 [info] [Wf04FY7BP8M9] (Trace=Tr04FHQLH3K9) Execution completed for workflow step 'Configure a trigger'2022-12-20 14:14:33 [info] [Fn04GN1S5CP2] (Trace=Tr04FRNYD1QW) Function execution completed for function 'Webhook Configurator'2022-12-20 14:14:33 [info] [Wf04FY7BP8M9] (Trace=Tr04FHQLH3K9) Execution completed for workflow 'Webhook Configurator'

Head to the channel you passed in the webhook request, and then add a reaction emoji to a message in the channel. If you receive an ephemeral message like the below, the event trigger for the "Main" workflow is properly configured

Create The "Configurator" Workflow (Link Trigger + Modal)

The above "Configurator" workflow worked very well, but it's not end-user-friendly. Also, allowing anyone to configure the trigger just by sending an HTTP POST request without any authentication mechanism is not secure enough.

To improve this, let's create a different version of the "Configurator" workflow, which can be invoked via a link trigger and accepts the channel ID list through a modal data submission.

First, add a new function named configure_interactive.ts. This function handles modal interactions plus saves the changes to the target trigger.

import { DefineFunction, Schema, SlackFunction } from "deno-slack-sdk/mod.ts";import {  createOrUpdateTrigger,  findTriggerToUpdate,} from "./manage_triggers.ts";import { joinAllChannels } from "./join_channels.ts";import { FunctionSourceFile } from "https://deno.land/x/[email protected]/mod.ts";export const def = DefineFunction({  callback_id: "configure_interactive",  title: "Configure a trigger using a modal",  source_file: FunctionSourceFile(import.meta.url),  input_parameters: {    properties: {      interactivity: { type: Schema.slack.types.interactivity },      workflowCallbackId: { type: Schema.types.string },    },    required: ["interactivity", "workflowCallbackId"],  },  output_parameters: { properties: {}, required: [] },});export default SlackFunction(def, async ({ inputs, client }) => {  const trigger = await findTriggerToUpdate(client, inputs.workflowCallbackId);  const channelIds = trigger?.channel_ids ?? [];  const response = await client.views.open({    interactivity_pointer: inputs.interactivity.interactivity_pointer,    view: {      "type": "modal",      "callback_id": "configure-workflow",      "title": { "type": "plain_text", "text": "My App" },      "submit": { "type": "plain_text", "text": "Confirm" },      "close": { "type": "plain_text", "text": "Close" },      "blocks": [        {          "type": "input",          "block_id": "channels",          "element": {            "type": "multi_channels_select",            "initial_channels": channelIds,            "action_id": "action",          },          "label": {            "type": "plain_text",            "text": "Channels to enable the main workflow",          },        },      ],    },  });  if (response.error) {    const error =      `Failed to open a modal in the configurator workflow. Contact the app maintainers with the following information - (error: ${response.error})`;    return { error };  }  return {    // To continue the interaction, you must return completed: false    completed: false,  };})  .addViewSubmissionHandler(    ["configure-workflow"],    async ({ inputs, client, view }) => {      const channelIds = view.state.values.channels.action.selected_channels;      try {        await createOrUpdateTrigger(          client,          inputs.workflowCallbackId,          channelIds,          (await findTriggerToUpdate(client, inputs.workflowCallbackId))?.id,        );      } catch (e) {        const error = `Failed to create/update a trigger due to ${e}.`;        return { error };      }      // If you don't need to invite your app's bot user to the channels,      // you can safely remove the following part:      const failure = await joinAllChannels(client, channelIds);      if (failure) {        const error = `Failed to join channels due to ${failure}.`;        return { error };      }      // Display the completion page      return {        response_action: "update",        view: {          "type": "modal",          "callback_id": "completion",          "title": { "type": "plain_text", "text": "My App" },          "close": { "type": "plain_text", "text": "Close" },          "blocks": [            {              "type": "section",              "text": {                "type": "mrkdwn",                "text":                  "*You're all set!*

The main workflow is now available for the channels :white_check_mark:", }, }, ], }, }; }, );

Add modal_configurator.ts, which defines the workflow that calls configure_interactive.ts function, and the workflow's link trigger.

import { DefineWorkflow, Schema } from "deno-slack-sdk/mod.ts";export const workflow = DefineWorkflow({  callback_id: "modal-configurator",  title: "Modal Configurator",  input_parameters: {    properties: { interactivity: { type: Schema.slack.types.interactivity } },    required: ["interactivity"],  },});import { def as ConfigureWithModal } from "./configure_interactive.ts";import { workflow as MainWorkflow } from "./main_workflow.ts";workflow.addStep(ConfigureWithModal, {  interactivity: workflow.inputs.interactivity,  // The callback_id here must be the one for "Main" workflow!  workflowCallbackId: MainWorkflow.definition.callback_id,});// Trigger to invoke the "Configurator" workflowimport { Trigger } from "deno-slack-api/types.ts";const trigger: Trigger<typeof workflow.definition> = {  type: "shortcut",  name: "Modal Configurator Trigger",  // The callback_id here must be the one for "Configurator" workflow!  workflow: `#/workflows/${workflow.definition.callback_id}`,  inputs: { interactivity: { value: "{{data.interactivity}}" } },};export default trigger;

Lastly, don't forget to add this workflow to manifest.ts:

import { Manifest } from "deno-slack-sdk/mod.ts";import { workflow as MainWorkflow } from "./main_workflow.ts";// Add thisimport { workflow as ConfiguratorWorkflow } from "./webhook_configurator.ts";import { workflow as ModalConfiguratorWorkflow } from "./modal_configurator.ts";export default Manifest({  name: "vibrant-orca-513",  description: "Configurator Demo",  icon: "assets/default_new_app_icon.png",  workflows: [MainWorkflow, ConfiguratorWorkflow, ModalConfiguratorWorkflow], // Add this  outgoingDomains: [],  botScopes: [    "commands",    "chat:write",    "chat:write.public",    // We're going to use "reaction_added" event trigger    "reactions:read",    // Required for `configure.ts`    "triggers:read",    "triggers:write",    "channels:join",  ],});

OK, the workflow is ready to use. Generate the link trigger by running slack triggers create --trigger-def modal_configurator.ts, and then share the link in a channel. When you start the workflow, you can easily configure the channel list in the modal UI:

Either way works! But for most use cases, I suggest using this modal configurator. Also, if you're interested in the details of the modal interactions, refer to my "Advanced Modals" tutorial for more information.

Wrapping Up

You've learned the following points with this hands-on tutorial:

  • Create a "Configurator" workflow, which manages another workflow's triggers

The complete project is available at https://github.com/seratch/slack-next-generation-platform-tutorials/tree/main/11_Trigger_Configurator

I hope you enjoy this tutorial! As always, if you have any comments or feedback, please feel free to let me know on Twitter (@seratch) or elsewhere I can check out!

Happy hacking with Slack's next-generation platform


Original Link: https://dev.to/seratch/slack-next-gen-platform-trigger-configurator-267m

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To