GithubHelp home page GithubHelp logo

ambro17 / slackify Goto Github PK

View Code? Open in Web Editor NEW
120.0 6.0 10.0 6.08 MB

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

Home Page: https://ambro17.github.io/slackify/

License: GNU General Public License v3.0

Python 51.83% Makefile 0.42% Dockerfile 0.34% Mako 47.41%
python slack bot flask framework slack-events slackapi modern-slack-bots slackify slack-modals

slackify's Introduction

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

slackify's People

Contributors

ambro17 avatar gsilvapt avatar rdbreak avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

slackify's Issues

Does not appear possible to use as a Blueprint

I have an existing flask app that includes a Blueprint (mounted at /webhooks/slack) for responding to slack commands. I'd love to replace that Blueprint with slackify, but it doesn't look like it's possible. Would you be open to changing Flack to be a Blueprint rather than a Flask subclass?

Add function to notify user of success/failure of operation taking in consideration posible unmet preconditions

Currently there's no single way to always send a message to a user. One can use the user id as the channel to chat.postMessage but it's not recommended by slack as it will be deprecated.

We should offer an abstraction over this.

It should be able to notify a user in the same chat (private or public channel, or direct message) that the command was invoked.
If certain precondition is not met, show a proper explanation and optionally send a message through slack bot using the user id as channel.

Add compatibility layer with aws chalice

Is your feature request related to a problem? Please describe.
AWS chalice has a decorator approach to infer the required infraestrcture and deploy it automagically

Describe the solution you'd like
If one wants to deploy the bot without managing the server, it would be possible

Describe alternatives you've considered
But it could also be an external package, not in this lib

Add instant deploy config with heroku

Several users asked for a beginner friendly, no config option to get a permanent url instead of heroku

Perhaps provide a heroku button if it is free

[BUG] respond's slackify expects `str` but `dict`s are passed

Describe the bug
When using the respond function from slackify's slack file, it expects to receive a url and a message. Typing was set to str when the entire documentation recommends sending a dictionary with the blocks.

This raises some warnings in the editor and is just confusing. Could we clarify and update the typing type to Dict (from Python's official Typing library)?

To Reproduce
Steps to reproduce the behavior:

  1. Use respond() method from slackify.slack.
  2. Send params as per documented: respond(payload['response_url'], {'blocks': [text_blok]})

Expected behavior
If the library expects a str, a str should be use. If a dictionary, a dictionary should be used.

Desktop (please complete the following information):

  • OS: MacOS
  • Python: 3.8.5
  • Version 1.0.0

Additional context
As I think this is a minor bug, I'm opening a quick PR to fix this issue. Will mention this issue for reference.

Events API change

The Events API is a system that apps can leverage to be notified of events and activity occurring in Slack. Changes are coming to the Events API that may impact your Slack apps.

On February 24th, 2021, event payloads from this system will send out authorization information related to the event in a new structure and format that returns a single authorization rather than a full list.

I got this email from Slack and I identified that your application still works with the old module.
Am I right here? Or I miss understand that email correctly.

how to pass parameters to an existing python file?

Hi,
I'm trying to deploy an app on Heroku.
The application contains 3 main files:

  • The web file, taken from your project.
  • My two main (separate) python files.

It seems I can't execute the other python file with some arguments ( I even tried to execute it w/o args. )
Heroku states that the file is not there. ( but, for sure it's there )
FileNotFoundError: [Errno 2] No such file or directory: 'main_time.py': 'main_time.py'
If you look at the pic, I do have the file.
I tried to excecute it by calling this function:
subprocess.call(['main_time.py', '2391', username, password])

The whole code could be found HERE and I'm on this issue for 4 days already, (newbie).

So, I think that an example of "how to execute external python files" is also needed. ( Python with arguments )

Appreciate all the help I will get here.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.