Automate the Boring Stuff with Slackbot (ver. 2)

Takanori Suzuki

PyCon TW 2023 / 2023 Sep 3


  • Background and Motivation for Slackbot

  • How to create simple bot

  • How to create interactive bot

  • How to extend bot using libs and APIs

Photos 📷 Tweets 🐦 👍

#pycontw / @takanory

Slides 💻

Why ver. 2 in the title?

Back to 2019

  • Title: “Automate the Boring Stuff with Slackbot”

  • Talk in 🇵🇭 🇹🇭 🇲🇾 🇯🇵 🇹🇼 🇸🇬 🇮🇩


And the 2023

  • Updated with latest information 🆕

  • Thanks to PyCon Taiwan staff and volunteers!! 👏

Who am I? 👤

takanory profile kuro-chan and kuri-chan

PyCon APAC 2023 in Tokyo, Japan 🇯🇵


Background and Motivation

Conference Tasks

  • I held PyCon JP(2014-2016) as Chair

  • Conference tasks:

    • 👨‍💻 Keynotes, Talks and Trainings

    • 🎫 Ticket sales and reception

    • 🏬 Venue and facility(WiFi, Video…)

    • 🍱 Foods, ☕️ Coffee, 🧁 Snacks and 🍺 Beers

Staff ask me the same things

  • 40+ staff

  • 🐣 NEW staff : 🐔 OLD staff = 50 : 50

  • 🐣 NEW staff ask me similar questions

Programmer is Lazy

I have an idea! 💡

Let’s create a secretary!!

Goal 🥅

  • How to create simple bot

  • How to create interactive bot

  • How to extend bot using libs and APIs

Why Slack ?

  • Launching the Slack app at any time 💻 📱

  • Easy to access

  • To do everything


You can create interactive bot

../_images/bot-result13.png ../_images/bot-result23.png

Simple integration with Incoming Webhooks 🪝

System overview


Create Incoming Webhooks Integration 🔧

Create Incoming Webhooks Integration

1. Create a Slack app

../_images/create-webhook1-13.png ../_images/create-webhook1-23.png
  • Enter name and choose workspace

  • Set app icon (optional)


2. Activate Incoming Webhooks


3. Add Webhook to Workspace

  • Click “Add New Webhook to Workspace”

  • Choose channel → Click “Allow”

  • Get Webhook URL:


Post message via Webhook URL 📬

Post message with cURL

$ curl -X POST -H 'Content-type: application/json' \
> --data '{"text":"Hello Slack!"}' \

Post message with Python

import json
from urllib import request

url = ""
message = {"text": "Hello from Python!"}
data = json.dumps(message).encode()
request.urlopen(url, data=data)

Post message with Requests

$ pip install requests
import requests

url = ""
message = {"text": "Hello from Requests!"}
r =, json=message)

Post message with Slack SDK

$ pip install slack-sdk
from slack_sdk.webhook import WebhookClient

url = ""
webhook = WebhookClient(url)
r = webhook.send(text="Hello from Slack SDK!")

Formatting text

from slack_sdk.webhook import WebhookClient

url = ""
webhook = WebhookClient(url)
# *bold*, <url|text>, :emoji: and etc.
r = webhook.send(text="*Hello* from "
  "<|Slack SDK>! :beer:")

Block Kit 🧱

Block Kit


Example of Block Kit

blocks = [{
  "type": "section",
  "text": {"type": "mrkdwn",
    "text": "*THANK YOU* for coming to my talk !:tada: Please tell me good *craft beer bar* :bow:",
  "fields": [
    {"type": "mrkdwn", "text": "*Love*"},
    {"type": "mrkdwn", "text": "*From*"},
    {"type": "plain_text", "text": ":beer:, Ferrets, LEGO"},
    {"type": "plain_text", "text": "Japan :jp:"},
response = webhook.send(blocks=blocks)

Block Kit Builder


Summary of Incoming Webhooks

  • Easy to post messages from programs 📬

  • Create complex messages with Block Kit 🧱

  • But one-way (program➡️Webhook➡️Slack)

Interactive bot 🤝

Connection protocols

Events API over HTTP


Socket Mode


Connection protocols

  • Events API over HTTP

  • Socket Mode 👈

Create bot user 🤖

Create bot user

  • Create bot user with Socket Mode

    1. Create a Slack app (same procedure)

    2. Enable Socket Mode

    3. Subscribe bot event

    4. Add Bot Token Scopes

    5. Install App to Workspace

  • Invite bot user to Slack channels

1. Create a Slack app

../_images/create-webhook1-13.png ../_images/create-webhook1-23.png
  • Enter name and choose workspace

  • Set app icon (optional)


2. Enable Socket Mode

  • Select “Socket Mode” → Turn toggle on

  • Enter token name → Click “Generate”

  • Get app-level token: xapp-...


3. Subscribe bot event

  • Select “Event Subscriptions” → Turn toggle on

  • Add “message.channels” to bot events

../_images/create-bot3-2-13.png ../_images/create-bot3-2-23.png

4. Add Bot Token Scopes

  • Select “OAuth & Permissions”

  • Click “Add on OAuth Scope”

  • Add “chat:write” to Bot Token Scopes

../_images/create-bot93.png ../_images/create-bot103.png

5. Install App to Workspace

  • Select “Install App” → Click “Install to Workspace”

  • Switch OAuth screen → Click “Allow” button

  • Get Bot Token: xoxb-...


Invite bot user to channels


Long and Complex !! 🤯

App Manifest ⚙️

App Manifest

Example of App Manifest

  name: beerbot2
    display_name: beerbot2
    always_online: false
      - channels:history
      - chat:write
      - message.channels
    is_enabled: true
  org_deploy_enabled: false
  socket_mode_enabled: true
  token_rotation_enabled: false

Get App Manifest

  • Select “App Manifest” menu


Create new app with App Manifest

  • Select “From an app manifest”

  • Select workspace → Click “Next”

../_images/app-manifest13.png ../_images/app-manifest23.png
  • Enter app manifest YAML

  • Review app summary → Click “Create”

../_images/app-manifest43.png ../_images/app-manifest53.png ../_images/app-manifest63.png
  • Install App to Workspace

  • Generate App-Level Token

../_images/app-manifest83.png ../_images/app-manifest93.png

Short and Reusable !! 🥳

Create bot with Bolt ⚡️

Bolt for Python

Install Bolt for Python

$ mkdir beerbot
$ cd beerbot
$ python3.11 -m venv env
$ . env/bin/activate
(env) $ pip install slack-bolt

Create a simple bot with Bolt
import os
from slack_bolt import App
from slack_bolt.adapter.socket_mode import SocketModeHandler

app = App(token=os.environ["SLACK_BOT_TOKEN"])

# match any message contains "Hi"
def handle_hi_message(message, say):
    say("Hi!!! I am beerbot! :beer:")

if __name__ == "__main__":
    app_token = os.environ["SLACK_APP_TOKEN"]
    SocketModeHandler(app, app_token).start()

Running bot

# Set 2 tokens in environment variables
(env) $ export SLACK_APP_TOKEN=xapp-...
(env) $ export SLACK_BOT_TOKEN=xoxb-...
(env) $ python
⚡️ Bolt app is running!

I can interact with the bot ! 🎉


Extend bot 🔧

@app.message() decolator

# match any message contains "Hi"
def handle_hi_message(message, say):
    say("Hi!!! I am beerbot! :beer:")

# match any message contains "cheers"
def handle_cheers_message(mesasge, say):
    say("Cheers! :beers:")



# mention
def handle_morning_message(message, say):
    user = message["user"]  # get user ID
    say(f"Good morning <@{user}>!")


Using regular expression

import random
import re

@app.message(re.compile(r"choice (.*)"))
def handle_choice(say, context):
    # get matched text from context["matches"]
    words = context["matches"][0].split()


Using regular expression

def handle_beer_or_tea(say, context):
    count = int(context["matches"][0])
    drink = context["matches"][1]
    say(f":{drink}:" * count)


Block Kit support

@app.message("follow me")
def handle_follow_me(message, say):
    blocks = [{
        "type": "section",
        "text": {
            "type": "mrkdwn",
            "text": "*Takanori Suzuki*\nFollow me! <|@takanory>"
        "accessory": {
            "type": "image",
            "image_url": "",
            "alt_text": "takanory"



import logging

def handle_log(message, say, logger):
    logger.debug(f"message: {message['text']}")
    say("logs exported!")

$ python
⚡️ Bolt app is running! log please write log

Events and Scopes 🔭

Events and Scopes

  • Can only receive events in Bot Events

  • Can only execute APIs allowed by Bot Token Scopes

Current Bot Events and Scopes

  • Events

    • message.channels

  • Scopes

    • channels:history

      • View messages in public channels

    • chat:write

      • Post message

  • Cannot read/write messages on private channels


Add Events and Scopes for private channels

  • Select “Event Subscriptions” → Click “Add Bot User Event”

  • Add message.groups event→ Click “Save Changes”


Add Events and Scopes for private channels

  • Select “OAuth & Permissions”

  • groups:history scope is automatically added


Add Events and Scopes for private channels

  • Reinstall app to workspace

../_images/add-events-and-scopes33.png ../_images/add-events-and-scopes43.png

Add Events and Scopes for private channels

  • Bot can read/write messages in private channel


To know user joined a channel

  • Add member_joined_channel event → Reinstall app

# A user joined a public or private channel
def member_joined(event, say):
    user = event["user"]  # get user ID
    say(f"Welcome <@{user}>! :tada:")

Add Emoji reaction

  • Add reactions:write scope → Reinstall app

def add_beer_emoji(client, message):
    # add reaction beer emoji

Summary of Events and Scopes

Case studies 📚

Calculator function using SymPy 🔢

Calculator function using SymPy

  • Motivation

    • I feel heavy to call a calculator app on my smartphone

    • It seems useful if Slack as a calculator

System overview


about SymPy
$ pip install sympy

calc() function using Sympy

from sympy import sympify, SympifyError

def calc(message, context, say):
        formula = context["matches"][0]
        result = sympify(formula)  # Simplifies formula
        if result.is_Integer:
            answer = int(result)  # Convert to integer
            answer = float(result)  # Convert to float
    except SympifyError:

Slack as a calculator !! 🎉


Plus-plus feature using Peewee ORM 👍

Plus-plus feature using Peewee ORM

  • Motivation

    • In PyCon JP, I want to make a culture that appreciates each other staff 👍

System overview


about Peewee
  • Simple and small ORM.

    • a small, expressive ORM

    • supports sqlite, mysql and postgresql


$ pip install peewee

from peewee import SqliteDatabase, Model, CharField, IntegerField

db = SqliteDatabase("plusplus.db")

class Plusplus(Model):
    name = CharField(primary_key=True)  # fields
    counter = IntegerField(default=0)

    class Meta:
        database = db

db.create_tables([Plusplus], safe=True)

plusplus() function using Peewee

from plusplus_model import Plusplus

# match "word++" pattern
def plusplus(say, context):
    name = context["matches"][0]
    # Get or create object
    plus, created = Plusplus.get_or_create(
        defaults={'counter': 0}
    plus.counter += 1
    say(f"Thank you {name}! (count: {plus.counter})")

I can appreciate it! 🎉


Search issues with Jira APIs 🔎

Search issues with Jira APIs

  • Motivation

    • Jira is very useful

    • Jira Web is slow

    • Search issues without Jira Web

System overview


about Python Jira

$ pip install jira


from jira import JIRA

url = ''
jira = JIRA(url, basic_auth=('email', 'API token'))

Search issues

@app.message(re.compile(r"^jira (.*)$"))
def jira_search(message, context, say):
    keywords = context["matches"][0]
    jql = f'text ~ "{keywords}" order by created desc'
    text = ""
    # get 5 recent issues
    for issue in jira.search_issues(jql, maxResults=5):
        issue_id = issue.key
        url = issue.permalink()
        summary = issue.fields.summary
        text += f"* <{url}|{issue_id}> {summary}\n"
    if not text:
        text = "No issues found"

Free from Jira web! 🎉


Create multiple issues from a template 📝

Create multiple issues from a template

  • Motivation

    • In pycamp event, 20+ issues are required for each event

    • Copying issues by hand is painful

    • Jira Web is slow (again)

System overview


Google Authorization is Complex

  • Create a Google Cloud project

    • Enable API(in this case: Google Sheets API)

    • Download credentials.json

  • Install Google Client Library

$ pip install google-api-python-client \
  google-auth-httplib2 google-auth-oauthlib

Google Authorization is Complex

  • Run

    • Select your Google account in Web browser

    • Click “Accept” button

    • Get token.json (finish!!)

$ python
Please visit this URL to authorize this application:
Name, Major:
Alexandra, English

Issue template


Get Spreadsheet data

from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build

SCOPES = [""]
SHEET = "1i3YQPObe3ZC_i1Jalo44_2_..."

@app.message("create issues")
def creaete_issues(message, say):
    creds = Credentials.from_authorized_user_file('token.json', SCOPES)
    service = build("sheets", "v4", credentials=creds)
    sheet = service.spreadsheets()
    result = sheet.values().get(spreadsheetId=SHEET, range="A2:C4").execute()
    for row in result.get("values", []):
        say(f"* Title: {row[0]}, Delta: {row[1]}")

Get Spreadsheet data


Create Jira issues

def creaete_issues(message, say):
    # --snip--
    today =
    for row in result.get("values", []):
        duedate = today + datetime.timedelta(days=int(row[1]))
        issue_dict = {"project": {"key": "ISSHA"},
                      "summary": row[0],
                      "description": row[2],
                      "duedate": f"{duedate:%Y-%m-%d}",
                      "issuetype": {"name": "Task"}}
        issue = jira.create_issue(fields=issue_dict)
        url = issue.permalink()
        say(f"* Create: <{url}|{issue.key}> {row[0]}\n")

Free from copying issues! 🎉

../_images/bot-sheet23.png ../_images/bot-sheet33.png

Account management of Google Workspace 👥

Account management of Google Workspace

  • Motivation

    • PyCon JP Association use domain with Google Workspace

    • I only use Google Admin web occasionally

    • I forgot to use admin screen

System overview


Update Google Authorization

  • Update a Google Cloud project

    • add Directory API

    • re-download credentials.json

  • Remove token.json

  • Add Directory API

    • Re-run

    • Get new token.json

Get user list

SCOPES = [""]

def user_list(message, say):
    creds = Credentials.from_authorized_user_file("token.json", SCOPES)
    service = build("admin", "directory_v1", credentials=creds)

    users_list = service.users().list(
        orderBy="email", maxResults=10, customer="my_customer").execute()

    for user in users_list.get("users", []):
        email = user["primaryEmail"]
        fullname = user["name"]["fullName"]
        say(f"* {email} {fullname}")

Get user list


Add user

@app.message(re.compile(r"user_add (\w+) (\w+) (\w+) (\w+)"))
def insert_user(message, say, context):
    creds = Credentials.from_authorized_user_file("token.json", SCOPES)
    service = build("admin", "directory_v1", credentials=creds)

    body = {
        "primaryEmail": context["matches"][0] + "",
        "password": context["matches"][1],
        "name": {
            "givenName": context["matches"][2],
            "familyName": context["matches"][3]
    say(f"User {body['primaryEmail']} created")

Add user

../_images/bot-user-add4.png ../_images/bot-user-add23.png

I can forget Google Admin! 🎉

Security Issue 🔓

  • Anyone can run it

  • Run only Slack Admin 🔒

Not-Admin cannot run

def user_list(message, say, client):
    # get user info:
    response = client.users_info(user=message["user"])
    user =["user"]
    if not user["is_admin"]:
        say("You are not an admin!")


Resolve a security issue 🎊


  • Simple bot using Incoming Webhooks

  • Interactive bot using Bolt for Python

  • Extend bot using libraries and APIs

Next Step 🪜

  • Let’s make your Slackbot

  • Let’s connect with libraries and APIs

  • Automate your Boring Stuff with Slackbot

Thank you! 🙏


translate command

$ pip install deepl
import deepl

translator = deepl.Translator(os.environ["DEEPL_AUTH_KEY"])

@app.message(re.compile(r"^translate (.*)"))
def translate(message, context, say):
    text = context["matches"][0]
    result = translator.translate_text(
        text, target_lang="EN-US")

Thank you! 🙏

../_images/bot-translate3.png code

@takanory takanory takanory takanory

takanory profile kuro-chan and kuri-chan