Beyond "Hello, World" Slackbots — A Better Way to Deal with Shortcuts and Limiting User Access

Lately I've been building a lot of Slack functionality for admins, like custom request flows and announcement posts. The standard app flow involves making global or message shortcuts or using slash commands to invoke the app.

There are significant problems with this approach.

  1. Not all users should be able to use all functions
  2. The actions menu starts to get really crowded with tons of shortcuts when you have a lot of apps
  3. People don't like and won't use slash commands, plus it's a headache to parse out a ton of arguments

So I've started building my bots to use their home tabs as entrypoints - we are moving towards using this approach as a standard across my company.

This dashboard approach involves building a few separate home_tab views. There's the standard home tab that everyone sees, and then there's the admin views. I've found it to be a pretty elegant solution that offers a lot more flexibility than a shortcut, while keeping the global space tidy.

This isn't a tutorial on how to build a Slack app! To get the most out of this post, you should already be fairly familiar with simple Slack app development and know how to launch modals, make views, respond to shortcuts/actions, etc. I am using Slack Bolt for Python for this write-up, but you can easily adapt this approach to your chosen languade and/or framework.

Prerequisites

Setup

Once you have your basic app set up and installed, you'll need to go into the api dashboard and enable the home tab under the App Home section. Note that it can take a little while for the event to start firing, and you need to both enable the home tab in that section and subscribe to the app_home_opened event under Event Subscriptions.

Now you can add a handler for when a user opens the home tab. Mine looks like:

    @app.event("app_home_opened")
    def update_home_tab(client, event, logger):
        try:
            user_id = event["user"]
            user_info = client.users_info(user=user_id)
            group_members = client.usergroups_users_list(usergroup=USERGROUP_ID)['users']
            is_app_admin = True if user_id in group_members else False
            client.views_publish(
                # Use the user ID associated with the event
                user_id=user_id,
                # Home tabs must be enabled in your app configuration
                view=views.home_tab_documentation(is_app_admin)
            )
        except Exception as e:
            print(f"Error publishing home tab: {e}")

I've decided to determine app admins based on their membership in a specific usergroup named after my app. You could also easily use other criteria - for example, I have also used the users.info method to allow all workspace admins to access a particular functionality.

Pick app admins

Some ways to make someone in an app admin:

  • Add all admins to a usergroup (I did that for this app) and check for membership with usergroups.users.list
  • Check to see if user is a member of a specific channel with conversations.members
  • Check to see if user meets any number of criteria via users.info, such as:
    • Is a workspace admin (or owner!)
    • Joined before of after a certain date
    • Has a particular name (Ok this one's a bit silly, but my point is you can use anything you want, really)
  • Any custom fields with users.profile.get
    • Will also allow you to access things like the user's status. Not completely sure how that would be useful, but could work for as-needed access to an app? Somehow work it into an on-call rotation? The sky's the limit!
  • If using a database, add column for any roles you want for your app

Write the home tab views

Once you've crafted your incredibly elegant admin selection process, it's pretty straightfoward to check:

def home_tab_documentation(is_app_admin): if is_app_admin: view = home_tab_admin() else: view = home_tab() return view

Then I have a basic home_tab and a home_tab_admin.

    def home_tab():
        return {
            "type": "home",
            "blocks": [
                {
                    "type": "header",
                    "text": {
                        "type": "plain_text",
                        "text": "APP TITLE",
                        "emoji": True,
                    },
                },
                {"type": "divider"},
                {
                    "type": "section",
                    "text": { 
                        "type": "mrkdwn",
                        "text": "Standard app documentation for everyone. It's a good idea to include some information as well as directing people to a help channel if they have questions, or to request access to the app.",
                    },
                }
            ],
        }

If the user is an app admin, I add a button to launch the various functionalities that normally would be in a global shortcut. (You could handle this any number of ways, including wrapping it all up into a single function with a bunch of conditional appends, but I like reading it this way.)

    def home_tab_admin():
        view = home_tab()
        admin_actions = {
            "type": "actions",
                "elements": [
                    {
                        "type": "button",
                        "text": {
                            "type": "plain_text",
                            "text": "Name of Action",
                            "emoji": True
                        },
                        "value": "value",
                        "action_id": "action_launcher"
                    },
                ]
        }
        view['blocks'].append(admin_actions)
        return view

And then handle them exactly like I would a shortcut, but in an action response.

    @app.action("action_launcher")
    def handle_action_launcher(client, ack, body):
        ack()
        # do stuff

You can use a button to launch a modal by using the body['trigger_id'] attribute; I mention this because I was originally passing in the action payload instead of the full body payload and couldn't find the trigger_id for the longest time.

Side note: I tend to write all my views as functions. You could just use a variable like home_tab = { code } but when developing complex apps you'll often want to be passing data in as arguments.

The final product can look something like:

Image of Slack Hometab Dashboard

Regular users will just see the title and text, while app admins will see the buttons. The example is my personal dev sandbox app, and has a bunch of experimental features that will later be broken off into their own apps as needed.

I used a boolean value of user/app-admin for this psuedo-tutorial, but you can get pretty complex with this if you need; you could check for users vs admins vs owners vs membership in various groups and alter the dashboard for each group depending on your needs. The sky's the limit! Get creative! (In the ad sales project on my projects page, I talk a little bit about more complex filtering processes using a database)


Useful links:

Set up your first bot in Slack Bolt

Shortcut Payload Reference Guide

© 2023 A Minor Studio