Receiving Shortcuts from Slack

A Shortcut is another way to allow users to interact directly with your app through Slack, there are two types you can configure:

  • Global shortcuts that are available from anywhere in Slack.
  • Message shortcuts that are shown only in message context menus.

While they are both called Shortcuts, they are very different artifacts in the world of Slack, accordingly, we'll walk through them separately in this guide. To learn more read through this guide from Slack.

In this guide, we will walk you through how to handle each of them in your integration. First, let's set up configure your Connector.

Configure your Connector

In Fusebit, update your Connector configurations:

  1. Update the Bot Token Scope to include commands
  2. Define an Integration ID that will be used to handle both the Acknowledgment & Message Responses
28462846

Slack Connector Configuration to handle Interactivity

Next, in the Slack App Dashboard, update your Slack App as well:

  1. In the dashboard for your app, navigate to Features > OAuth & Permissions > Scopes and add the commands OAuth scope.
725725

Slack App Dashboard - OAuth Scopes

For your app, you will also have to enable "Slash Commands" or "Interactivity & Shortcuts" from this dashboard based on your apps needs. It's fairly straightforward to do, and you can also follow along in the guide's available from Slack.

Set up your Integration

There are two main components to handling interactivity:

  • Acknowledgement Response: Must happen within 3000 ms or the user will get a timeout.
  • Message Response: Can take longer and must be targeted back to the original Slack Message.

The Integration ID you defined in your Connector will be responsible for Acknowledgement & Message Responses.

Message Shortcut

Message shortcuts allow users to invoke your app directly from a Slack message as they are shown in the context menu of any non-ephemeral message. They retain the context of the source message from which they were initiated. This makes it ideal for when you have a workflow that relies on that context to work.

For example, users might quickly generate tasks from a posted message, or send messages to external services.

15601560

Interactivity Example: Message Shortcuts

Acknowledgement Response

As mentioned above, Slack requires all incoming interactivity requests to receive an HTTP 200 Acknowledgement within 3000 ms, you must also send back a custom message that will be displayed to the user.

router.post('/api/fusebit/webhook/event/immediate-response/', async (ctx) => {
  
    const acknowledgement_response = {
      text: `:hourglass_flowing_sand: Running...`,
      response_type: 'ephemeral',
    };
  
    // Optional Explicit Check to see if it's a message action
    if (ctx.req.body.type && ctx.req.body.type == 'message_action') {
      return acknowledgement_response;
    }
});

Message Response

In addition to the static immediate 'Acknowledgement' Response, you will want to respond to the message with contextual data based on what the User has requested.

📘

Finding the Tenant ID from Slack

Most incoming Requests include a response_url that is used to respond back directly without requiring any authentication. The slackClient object provided by Fusebit uses the officially recommended @slack/webhook package to facilitate this via theintegration.webhook.send method.

To find the associated Tenant ID and make an authenticated call instead, you can use the integration.webhook.searchInstalls method using the tags received (e.g. user_id, team_id) in the request body.

To respond back to the message, you can leverage the Fusebit's Event Handler to receive and respond back to the message:

integration.event.on('/:componentName/webhook/message_action', async (ctx) => {
  await integration.webhook.send(ctx, { text: `Shortcut Received!` });
});
integration.event.on('/:componentName/webhook/message_action', async (ctx) => { 
try {
    // Retrieve Tags from Request Bofy
    const { team_id, user_id, api_app_id: app_id, channel_id } = ctx.req.body.data;
  
    // Search for the associated Install
    const installs = await integration.webhook.searchInstalls(ctx, 'slackConnector', {app_id,team_id,user_id});
    const slackClient = await integration.service.getSdk(ctx, 'slackConnector', installs[0].id);
  
    // Perform your Business Logic and Send a Message
    await slackClient.chat.postMessage({
      text: 'Command processing finished',
      channel: channel_id,
    });

  } catch (error) {
    // Detect if the error is coming because no Installs were returned
    if (error.statusCode === 404) {
      await integration.webhook.send(ctx, { text: 'User is not authorized, please re-install the Slack Integration Here: INSTALL_URL' });
    } else {
      // Something else failed, log the error and inform the user
      console.log(error.message);
      await integration.webhook.send(ctx, { text: 'Something went wrong!' });
    }
  }
 });

For all incoming messages, the request body will consist of two items:

  • Slack Interactions Payload under the data object.
  • Fusebit Event Metadata which includes items such as related Integration ID, related Installation IDs etc.

Here is an example of an incoming Request Body for a message action:

{
  data: {
    type: 'message_action',
    token: 'LVGaG1ijm6mcT4ZdxJ8MawUz',
    action_ts: '1655152734.598849',
    team: { id: 'T02A14HFYDP', domain: 'shehzadakbar' },
    user: {
      id: 'U029KE88QCV',
      username: 'akbar.shehzad',
      team_id: 'T02A14HFYDP',
      name: 'akbar.shehzad'
    },
    channel: { id: 'C0297PH1H55', name: 'general' },
    is_enterprise_install: false,
    enterprise: null,
    callback_id: 'kudos_message_shortcut',
    trigger_id: '3662041308386.2341153542465.6de78fb84808d4ae20e3ea6b08c96a8a',
    response_url: 'https://hooks.slack.com/app/T02A14HFYDP/3674697348929/34vIHPziy4rFLEl1XVraFjbp',
    message_ts: '1655144432.721299',
    message: {
      bot_id: 'B03FDU8BZTJ',
      type: 'message',
      text: 'Hey shehzad, thank you for leaving kudos for @matthew!',
      user: 'U03E9J02B0F',
      ts: '1655144432.721299',
      app_id: 'A03FCRX58P2',
      team: 'T02A14HFYDP',
      bot_profile: [Object],
      blocks: [Array]
    }
  },
  eventType: 'message_action',
  entityId: 'SlashCommandsDocs',
  webhookEventId: 'webhook/MySalesforceIntegration/team_id/T02A14HFYDP',
  webhookAuthId: 'team_id/T02A14HFYDP',
  installIds: [
    'ins-1f02fc07f41e4d73957c952147300688',
    'ins-455ecfd3b413f152eb3d96dd804fbcad'
  ]
}

👍

Good Job!

Your app should now be set up properly to handle and respond to Message Shortcuts!

Global Shortcut

Global shortcuts can be initiated from the shortcuts button in the message composer, or from within search. These type of shortcuts are intended to trigger workflows that can operate without the context of a channel or message.

For example, users might trigger a global shortcut to create a calendar event or view their upcoming on-call shifts.

746746

Interactivity Example: Global Shortcuts

Acknowledgement Response

As mentioned above, Slack requires all incoming interactivity requests to receive an HTTP 200 Acknowledgement within 3000 ms, you must also send back a Modal that will be displayed to the user.

🚧

Special Use Case for Global Shortcuts

In addition to the regular ACK, there is additional item to consider for Global Shortcuts only. As there is no message to respond back to, instead of sending a custom message, you are required to send back a Modal within 3000ms as well and can only be done using an Authenticated Slack SDK.

We can use our immediate-response endpoint and Fusebit's built in storage SDK to help facilitate this by opening a view and then updating it later in our message response. Learn more about using Modals with this guide from Slack.

In this acknowledgement message, we perform the following steps:

  • Check to see if it's a Global Shortcut
  • Find an installation tied to the Team ID
  • Open a View Modal with "Now Loading..." text
  • Store the View ID in the local integration storage using Team ID, User ID & Trigger ID
// Acknolwedgement Response
router.post('/api/fusebit/webhook/event/immediate-response/', async (ctx) => {
 // Explicit Check to see if it's a Global Shortcut
 if (ctx.req.body.type && ctx.req.body.type == 'shortcut') {

      // Retrieve Install ID using Team ID
      const team_id = ctx.req.body.user.team_id
      const installs = await integration.webhook.searchInstalls(ctx, 'slackConnector', {team_id});
      const slackClient = await integration.service.getSdk(ctx, 'slackConnector', installs[0].id);
   
      // Open a "Loading" Modal
      const result = await slackClient.views.open({
        trigger_id: ctx.req.body.trigger_id,
        // Defining View Objects: https://api.slack.com/reference/surfaces/views
        view:
        {
          "type": "modal",
          "title": {
            "type": "plain_text",
            "text": "Workplace check-in"
          },
          "close": {
            "type": "plain_text",
            "text": "Cancel"
          },
          "blocks": [
            {
              "type": "section",
              "text": {
                "type": "plain_text",
                "text": ":man-biking: Now loading..."
              }
            }
          ]
        }
      });
   
      // Store the View ID in the Integration Storage SDK with the Associated User ID
      const user_id = ctx.req.body.user.id
      await integration.storage.setData(ctx, `${team_id}/${user_id}/${ctx.req.body.trigger_id}/viewId`, { data : { view_id: result.view.id }});
      return result;
    }

});

Message Response

In addition to the static immediate 'Acknowledgement' Response, you will want to respond to the shortcut with contextual data based on what the User has requested. To respond back, you can leverage the Fusebit's Event Handler to retrieve the TenantID and update the Modal with the information.

In this message response, we perform the following steps:

  • Wait for Immediate Response to finish so we can be sure the View ID is set properly
  • Retrieve the User, Team, Trigger & View IDs from the request body
  • Retrieve the View ID from Integration Storage
  • Find the Associated Install (if none is found, searchInstalls will throw a 404 so you can handle it)
  • Update the View (using a separate function)
integration.event.on('/:componentName/webhook/shortcut', async (ctx) => {
  // Wait for the Immediate Response to Resolve first
  await new Promise(resolve => setTimeout(resolve, 3000));

  // Deconstruct Request Body as needed
  const user_id = ctx.req.body.data.user.id;
  const team_id = ctx.req.body.data.user.team_id;
  const trigger_id = ctx.req.body.data.trigger_id;
  const viewId = await integration.storage.getData(ctx, `${team_id}/${user_id}/${trigger_id}/viewId`);
  const viewIdToUpdate = viewId.data.view_id;
  

  try {

    // Find the Install associated to the user
    const installs = await integration.webhook.searchInstalls(ctx, 'slackConnector', {team_id,user_id});
    const slackClient = await integration.service.getSdk(ctx, 'slackConnector', installs[0].id);

    // Do whatever your business logic needs to do
    const modal_text = 'Success! We have received your Shortcut'
    return slackUpdateView(ctx, viewIdToUpdate, modal_text, slackClient);
    
  } catch (error) {
    // If we can not find an Install associated to the specific User, we can ask to re-authorize
    if (error.statusCode === 404) {

      // Find any Install Associated to the Team (instead of user)
      const installs = await integration.webhook.searchInstalls(ctx, 'slackConnector', { team_id });
      const slackClient = await integration.service.getSdk(ctx, 'slackConnector', installs[0].id);

      // Ask user to Authorize
      const modal_text = 'User is not authorized, please re-install the Slack Integration Here: INSERT_INSTALL_URL_HERE'
      return slackUpdateView(ctx, viewIdToUpdate, modal_text, slackClient);
    } else {
      // Something else failed, log the error and handle accordingly. 
      console.log(error);
    }

    return updateView;
  }
});
async function slackUpdateView(ctx, viewId, modal_text, slackClient) {

    const updateView = await slackClient.views.update({
      view_id: viewId,
      view: {
        "type": "modal",
        "callback_id": "your-callback-id",
        "submit": {
          "type": "plain_text",
          "text": "Submit"
        },
        "close": {
          "type": "plain_text",
          "text": "Cancel"
        },
        "title": {
          "type": "plain_text",
          "text": "Workplace check-in"
        },
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "plain_text",
              "text": `${modal_text}`
            }
          }
        ]
      }
    })

    return updateView;
}

👍

Good Job!

Now you can configure Message & Global Shortcuts for your Slack App and handle them in Fusebit!