Python: пишем Slackbot

Лично мне он пригодится для автоматизации различных процессов. Каких не скажу. Для работы бота нам понадобится веб-сервер т.е. бот должен быть доступен с внешки. Когда в slack будем давать команды, то slack api будет дергать нашего бота, который в ответ должен будет возвращать что-либо.

Создаем приложение

Идем на стрницу api.slack.com/apps и регистрируем приложение.

Задаем название и выбираем workspace для которого оно создается.

В Features->Oauth & Permissions в Add features and functionality выбираем Bots.

В OAuth & Permissions->Scopes->Bot token scopes добавляем: app_mentions:read, channels:history, channels:read, channels:write, chat:write, commands, im:history, im:read, im:write. Далее нажимаем "Install App to Workspace".

После установки приложения либо сразу высветится Bot User OAuth Access Token либо найти его можно в OAuth & Permissions.

Если пойдем в Basic Information, то найдем Signing Secret и Verification Secret (в скрине последний отсутвует). Client ID не понадобится.

Конфигурация

Теперь создадим config.ini приложения и соответственно конфиг надо заполнить своими данными:

[SLACK]
SIGNING_SECRET = тут signing secret
VERIFICATION_TOKEN = тут verification secret
OAUTH_TOKEN = тут bot user oauth access token
DOMAIN = тут домен по которому будет доступен бот, например slackbot.example.com
LISTEN = 127.0.0.1
PORT = 3000

[LOG]
LOG_FILE = main.log
LOG_SIZE = 10000000
LOG_ROTATE = 5

Подразумевается что у нас уже есть адрес slackbot.example.com потому пойдем и настроим nginx:

upstream bot {
    server 127.0.0.1:3000;
}
...
location / {
    proxy_pass http://bot;
    proxy_redirect off;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_set_header X-NginX-Proxy true;
}

Теперь накидаем стартовый скрипт в /etc/systemd/system/slackbot.service:

[Unit]
Description=Slackbot
After=network.target

[Service]
Type=simple
PIDFile=/var/run/slackbot.pid
WorkingDirectory=/opt/slackbot
ExecStart=/opt/slackbot/main.py
Restart=always
User=nobody
Group=nobody

[Install]
WantedBy=multi-user.target

Тело бота

Установка пакетов:

pip3 install flask flask_ini slackeventsapi slackclient

Ну и собственно накидаем main.py т.е. каркас приложения, который на приветствие будет отвечать нам:

#!/usr/bin/python3

from flask import Flask, Response, jsonify, request
from flask_ini import FlaskIni
from slackeventsapi import SlackEventAdapter
import os
from threading import Thread
from slack import WebClient
import logging
from logging.handlers import RotatingFileHandler

app = Flask(__name__)

with app.app_context():
    app.iniconfig = FlaskIni()
    app.iniconfig.read('./config.ini')

greetings = ["hi", "hello", "hello there", "hey"]

SLACK_SIGNING_SECRET = app.iniconfig.get('SLACK', 'SIGNING_SECRET')
SLACK_TOKEN = app.iniconfig.get('SLACK', 'OAUTH_TOKEN')
VERIFICATION_TOKEN = app.iniconfig.get('SLACK', 'VERIFICATION_TOKEN')
DOMAIN = app.iniconfig.get('SLACK', 'DOMAIN')
HOST = app.iniconfig.get('SLACK', 'LISTEN')
PORT = app.iniconfig.getint('SLACK', 'PORT')
LOG_FILE = app.iniconfig.get('LOG', 'LOG_FILE')
LOG_SIZE = app.iniconfig.getint('LOG', 'LOG_SIZE')
LOG_ROTATE = app.iniconfig.getint('LOG', 'LOG_ROTATE')

slack_client = WebClient(SLACK_TOKEN)

logging.basicConfig(
    handlers=[RotatingFileHandler(LOG_FILE, maxBytes=LOG_SIZE, backupCount=LOG_ROTATE)],
    level=logging.INFO,
    format="[%(asctime)s] %(levelname)s [%(name)s.%(funcName)s:%(lineno)d] %(message)s",
    datefmt='%Y-%m-%dT%H:%M:%S'
)

app.logger.info("============= Slackbot start! =============")

@app.errorhandler(Exception)
def server_error(err):
    app.logger.error(f"App error: {err}")
    return "Forbidden", 403

@app.errorhandler(ZeroDivisionError)
def server_error(err):
    app.logger.error(f"Server error: {err}")
    return "Cannot divide by 0", 500

@app.route("/")
def event_hook(request):
    json_dict = json.loads(request.body.decode("utf-8"))
    if json_dict["token"] != VERIFICATION_TOKEN:
        app.logger.error(f"Client error: {json_dict}\nReturn status 403")
        return {"status": 403}

    if "type" in json_dict:
        if json_dict["type"] == "url_verification":
            response_dict = {"challenge": json_dict["challenge"]}
            return response_dict
    app.logger.error(f"Server error: {json_dict}\nReturn status 500")
    return {"status": 500}
    return

slack_events_adapter = SlackEventAdapter(
    SLACK_SIGNING_SECRET, "/slack/events", app
)  

@slack_events_adapter.on("app_mention")
def handle_message(event_data):
    def send_reply(value):
        event_data = value
        message = event_data["event"]
        if message.get("subtype") is None:
            command = message.get("text")
            channel_id = message["channel"]
            # event commands (mentions)
            if any(item in command.lower() for item in greetings):
                message = (
                    "<@%s> ready to serve!"
                    % message["user"]
                )
                slack_client.chat_postMessage(channel=channel_id, text=message)
    thread = Thread(target=send_reply, kwargs={"value": event_data})
    thread.start()
    return Response(status=200)

if __name__ == "__main__":
  app.run(debug=True, host=HOST, port=PORT)

Запускаем бота:

systemctl daemon-reload
systemctl enable slackbot
systemctl start slackbot

В Event subscriptions включаем Enable events и в Request URL указываем: https://slackbot.example.com/slack/events, чтобы включить реакции на события. Если все хорошо, то статус Request URL поменяется на Verified иначе получается, что бот еще не доступен для внешки и slack api не может достучаться до бота. В Subscribe bot events добавляем: app_mention, message.im, message.channels.

Теперь в slackchat создаем тестовый канал...ну пусть будет #bot_test и приглашаем туда бота:

/invite slackbot

Где slackbot это название созданного ранее приложения в api.slack.com.
Теперь если поприветствуем его командой @slackbot hi, то он ответ "ready to serve".
Собственно после блока кода:

if any(item in command.lower() for item in greetings):
                message = (
                    "<@%s> ready to serve!"
                    % message["user"]
                )
                slack_client.chat_postMessage(channel=channel_id, text=message)

можно добавлять свой код. Например можем добавить такой код:

            if "blabla" in command:
                message = "Хватит блакать!"
                slack_client.chat_postMessage(channel=channel_id, text=message)

Теперь если написать @slackbot blabla, то он ответит "Хватит блакать!".

Также боты в slack поддерживают команды с слешем и ответ на такие команды будет виден только вам. Для этого в Slash commands регистрируем команду скажем test и Request URL: https://slackbot.example.com/test. После slack_events_adapter добавляем следующий код:

# slash commands
@app.route('/test', methods=['POST'])
def test():
    if request.form['token'] == VERIFICATION_TOKEN and request.form['text']:
        command = request.form['text']
        payload = {'text': 'Это тестовое сообщение'}
        return jsonify(payload)

Теперь если в slackchat набрать команду /test, то бот ответит "Это тестовое сообщение".

В плане Slack commands еще есть такая ремарка, что если уже кто-то в твоем workspace создал бота и уже зарегистрировал команду /test, то команда /test твоего бота не будет работать. Потому такие вещи надо будет проверять (тут только методом тыка).

Оригинальный источник: https://medium.com/developer-student-clubs-tiet/how-to-build-your-first-slack-bot-in-2020-with-python-flask-using-the-slack-events-api-4b20ae7b4f86