Skip to content

Slackify: Lightweight framework to quickly develop modern Slack bots πŸš€

License

Notifications You must be signed in to change notification settings

Ambro17/slackify

Build Codecov pre-commit

Slackify

Slackify is a lightweight framework that lets you quickly develop modern Slack bots focusing in what you want instead of struggling with how to do it

Installation

python3 -m pip install slackify

Requires python3.6+

Documentation

You can read Slackify docs here

Quickstart

1. 1-Click Deploy

Deploy

The server will listen at <heroku_url>/ for commands/actions and <heroku_url>/slack/events for slack events

This setup uses flask builtin server which is NOT suited for production. Replace it by gunicorn or similar when ready to ship

2. Manual deploy

Create a file named quickstart.py with the following content and then run python quickstart.py

from time import sleep
from flask import Flask
from slackify import (
    Slackify,
    async_task,
    reply_text
)

app = Flask(__name__)
slackify = Slackify(app=app)


@slackify.command
def hello():
    my_background_job()
    return reply_text('Hello from Slack!')


@async_task
def my_background_job():
    """My long background job"""
    sleep(15)
    return

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Now the server is already running, but we need to make it reachable by slack. To do so follow these steps:

  1. Create a slack app
  2. Download ngrok* and run ngrok http 5000 to create a https proxy to localhost
  3. Create a slash command and set the url to ngrok's https url of step #1
  4. Write /hello to your new slack bot and let the magic begin ✨

*This is a development setup so you can quickly see your code changes in slack without the need to redeploy your whole site. Once your bot is ready for production you should update your commands url to a permanent one. Heroku might be a good choice if you are just getting started as it has a generous free tier.

Features

Full example

If you want a full stack example showcasing all functionality. It includes:

  • A hello command that shows interactive buttons
  • Callbacks for each interactive button click
  • A register command that opens a new slack modal
  • A callback on modal form submission
  • A shortcut to roll a dice and get a random number
  • An event handler that echoes reactions to messages.
  • A greeting whenever someone says hello in a channel where the bot is present.

Remember to export BOT_TOKEN=xoxb-your-bot-secret to enable slack api calls.

import json
import os
import random

from flask import Flask
from slackify import (
    ACK, OK, Slackify, async_task, block_reply,
    request, respond, text_block, Slack
)

app = Flask(__name__)
slackify = Slackify(app=app)
cli = Slack(os.getenv('BOT_TOKEN'))


@slackify.command
def hello():
    """Send hello message with question and yes no buttons"""
    YES = 'yes'
    NO = 'no'
    yes_no_buttons_block = {
        "type": "actions",
        "elements": [
            {
                "type": "button",
                "text": {
                    "type": "plain_text",
                    "emoji": True,
                    "text": "Yes"
                },
                "style": "primary",
                "value": "i_like_bots",
                "action_id": YES
            },
            {
                "type": "button",
                "text": {
                    "type": "plain_text",
                    "emoji": True,
                    "text": "No"
                },
                "style": "danger",
                "value": "i_dont_like_bots",
                "action_id": NO
            }
        ]
    }
    blocks = [
        text_block('Do you like Bots?'),
        yes_no_buttons_block
    ]
    return block_reply(blocks)


@slackify.action("yes")
def yes(payload):
    """Run this if a user clicks yes on the message above"""
    text_blok = text_block('Super! I do too :thumbsup:')
    respond(payload['response_url'], {'blocks': [text_blok]})
    return OK


@slackify.action("no")
def no(payload):
    """Run this if a user clicks no on the message above"""
    text_blok = text_block('Boo! You are so boring :thumbsdown:')
    respond(payload['response_url'], {'blocks': [text_blok]})
    return OK


@slackify.command
def register():
    """Open a registration popup that asks for username and password."""
    username_input_block = {
        "type": "input",
        "block_id": "username_block",
        "element": {
            "type": "plain_text_input",
            "placeholder": {
                "type": "plain_text",
                "text": "Enter your username"
            },
            "action_id": "username_value"
        },
        "label": {
            "type": "plain_text",
            "text": "πŸ‘€ Username",
            "emoji": True
        }
    }
    password_input_block = {
        "type": "input",
        "block_id": "password_block",
        "element": {
            "type": "plain_text_input",
            "placeholder": {
                "type": "plain_text",
                "text": "Enter your password"
            },
            "action_id": "password_value"
        },
        "label": {
            "type": "plain_text",
            "text": "πŸ”‘ Password",
            "emoji": True
        }
    }
    modal_blocks = [
        username_input_block,
        password_input_block,
    ]
    callback_id = 'registration_form'
    registration_form = {
        "type": "modal",
        "callback_id": callback_id,
        "title": {
            "type": "plain_text",
            "text": "My First Modal",
            "emoji": True
        },
        "submit": {
            "type": "plain_text",
            "text": "Register",
            "emoji": True
        },
        "close": {
            "type": "plain_text",
            "text": "Cancel",
            "emoji": True
        },
        "blocks": modal_blocks
    }
    cli.views_open(
        trigger_id=request.form['trigger_id'],
        view=registration_form
    )
    return OK


@slackify.view("registration_form")
def register_callback(payload):
    """Handle registration form submission."""
    response = payload['view']['state']['values']
    text_blok = text_block(
        ':heavy_check_mark: You are now registered.\n
        f'Form payload:\n```{response}```'
    )
    send_message(cli, [text_blok], payload['user']['id'])
    return ACK


@async_task
def send_message(cli, blocks, user_id):
    return cli.chat_postMessage(channel=user_id, user_id=user_id, blocks=blocks)


@slackify.shortcut('dice_roll')
def dice_roll(payload):
    """Roll a virtual dice to give a pseudo-random number"""
    dice_value = random.randint(1, 6)
    msg = f'🎲 {dice_value}'
    send_message(
        cli,
        blocks=[text_block(msg)],
        user_id=payload['user']['id']
    )
    return ACK


@slackify.event('reaction_added')
def echo_reaction(payload):
    """Adds the same reaction as the user"""
    event = payload['event']
    reaction = event['reaction']
    cli.reactions_add(
        name=reaction,
        channel=event['item']['channel'],
        timestamp=event['item']['ts']
    )


@slackify.message('hello')
def say_hi(payload):
    event = payload['event']
    cli.chat_postMessage(
        channel=event['channel'],
        text='Hi! πŸ‘‹'
    )

Dependency Injection

As you add more and more commands you will find yourself parsing slack's request over and over again.

Slackify offers shortcut for this using dependency injection.

@slackify.command
def hello(command, command_args, response_url):
    return reply_text(
        f"You called `{command} {command_args}`. Use {response_url} for delayed responses"
    )

Your view function will now receive the slash command, the arguments and the response_url upon invocation. Pretty cool, right?

If you are a user of pytest, this idea is similar to pytest fixtures

See examples/injection.py for the full example

Blueprint Support

If you already have a Flask app, you can attach flask functionality slackifying your blueprint

# slack_blueprint.py
from slackify import Slackify, reply_text, Blueprint

bp = Blueprint('slackify_bp', __name__, url_prefix='/slack')
slackify = Slackify(app=bp)


@slackify.command
def hello():
    return reply_text('Hello from a blueprint')


# app.py
from flask import Flask
from slack_blueprint import bp

def create_app():
    app = Flask(__name__)
    app.register_blueprint(bp)
    return app

Note: You must import Blueprint from slackify instead of flask to get it working

Dependencies

This projects uses Flask as the web server and slackclient (The official python slack client) as slack's API wrapper. It also uses pyee for async handling of events