Automate the Boring Stuff with Slackbot (ver. 2)
Takanori Suzuki
PyCon TW 2023 / 2023 Sep 3
Agenda
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? 👤
Takanori Suzuki / 鈴木 たかのり ( @takanory)
PyCon JP Association: Chair
BeProud Inc.: Director / Python Climber
Python Boot Camp, Python mini Hack-a-thon, Python Bouldering Club
Love: Ferrets, LEGO, 🍺 / Hobby: 🎺, 🧗♀️
PyCon APAC 2023 in Tokyo, Japan 🇯🇵

Date: 2023 Oct 27-29
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


Simple integration with Incoming Webhooks 🪝
System overview

Create Incoming Webhooks Integration 🔧
Create Incoming Webhooks Integration
Generate Webhook URL
Create a Slack app
Activate Incoming Webhooks in the app
Add Webhook to Workspace
1. Create a Slack app


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:
https://hooks.slack.com/...

Post message via Webhook URL 📬
Post message with cURL
$ curl -X POST -H 'Content-type: application/json' \
> --data '{"text":"Hello Slack!"}' \
> https://hooks.slack.com/services/T000...

Post message with Python
see: urllib.request
import json
from urllib import request
url = "https://hooks.slack.com/services/T000..."
message = {"text": "Hello from Python!"}
data = json.dumps(message).encode()
request.urlopen(url, data=data)

Post message with Requests
see: Requests
$ pip install requests
import requests
url = "https://hooks.slack.com/services/T000..."
message = {"text": "Hello from Requests!"}
r = requests.post(url, json=message)

Post message with Slack SDK
see: Python Slack SDK
$ pip install slack-sdk
from slack_sdk.webhook import WebhookClient
url = "https://hooks.slack.com/services/T000..."
webhook = WebhookClient(url)
r = webhook.send(text="Hello from Slack SDK!")

Formatting text
from slack_sdk.webhook import WebhookClient
url = "https://hooks.slack.com/services/T000..."
webhook = WebhookClient(url)
# *bold*, <url|text>, :emoji: and etc.
r = webhook.send(text="*Hello* from "
"<https://slack.dev/python-slack-sdk/|Slack SDK>! :beer:")

Block Kit 🧱
Block Kit

see: 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
Events API over HTTP

Socket Mode

see: Intro to Socket Mode
Connection protocols
Events API over HTTP
Socket Mode 👈
Create bot user 🤖
Create bot user
Create bot user with Socket Mode
Create a Slack app (same procedure)
Enable Socket Mode
Subscribe bot event
Add Bot Token Scopes
Install App to Workspace
Invite bot user to Slack channels
1. Create a Slack app


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


4. Add Bot Token Scopes
Select “OAuth & Permissions”

Click “Add on OAuth Scope”

Add “chat:write” to Bot Token Scopes


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
YAML-formatted configuration for Slack apps
Example of App Manifest
display_information:
name: beerbot2
features:
bot_user:
display_name: beerbot2
always_online: false
oauth_config:
scopes:
bot:
- channels:history
- chat:write
settings:
event_subscriptions:
bot_events:
- message.channels
interactivity:
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”


Enter app manifest YAML

Review app summary → Click “Create”



Install App to Workspace

Generate App-Level Token


Short and Reusable !! 🥳
Create bot with Bolt ⚡️
Bolt for Python
Python framework to build Slack app in a flash
Developped by Slack
see:
The Bolt family of SDKs (JavaScript, Java)
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"
@app.message("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 app.py
⚡️ Bolt app is running!
I can interact with the bot ! 🎉

Extend bot 🔧
@app.message()
decolator
# match any message contains "Hi"
@app.message("Hi")
def handle_hi_message(message, say):
say("Hi!!! I am beerbot! :beer:")
# match any message contains "cheers"
@app.message("cheers")
def handle_cheers_message(mesasge, say):
say("Cheers! :beers:")

mention
# mention
@app.message("morning")
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()
say(random.choice(words))

Using regular expression
@app.message(re.compile(r"(\d+)\s*(beer|tea)"))
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! <https://twitter.com/takanory|@takanory>"
},
"accessory": {
"type": "image",
"image_url": "https://pbs.twimg.com/profile_images/192722095/kurokuri_400x400.jpg",
"alt_text": "takanory"
}}]
say(blocks=blocks)

Logging
import logging
logging.basicConfig(level=logging.DEBUG)
@app.message("log")
def handle_log(message, say, logger):
logger.debug(f"message: {message['text']}")
say("logs exported!")
$ python app.py
⚡️ Bolt app is running!
DEBUG:app.py:handle_log:message: log
DEBUG:app.py:handle_log:message: 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
Current Bot Events and Scopes
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


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
@app.event("member_joined_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
@app.message("beer")
def add_beer_emoji(client, message):
# add reaction beer emoji
client.reactions_add(
channel=message["channel"],
timestamp=message["ts"],
name="beer",
)

Summary of Events and Scopes
To receive new events
To use new API with new scopes
Add events and/or scopes → Reinstall app
see: Events API types
see: Permission 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

SymPy: Python library for symbolic mathematics
$ pip install sympy
calc() function using Sympy
from sympy import sympify, SympifyError
@app.message(re.compile(r"^([-+*/^%!().\d\s]+)$"))
def calc(message, context, say):
try:
formula = context["matches"][0]
result = sympify(formula) # Simplifies formula
if result.is_Integer:
answer = int(result) # Convert to integer
else:
answer = float(result) # Convert to float
say(f"{answer:,}")
except SympifyError:
pass
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
plusplus_model.py
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.connect()
db.create_tables([Plusplus], safe=True)
plusplus() function using Peewee
from plusplus_model import Plusplus
# match "word++" pattern
@app.message(re.compile(r"^(\w+)\+\+"))
def plusplus(say, context):
name = context["matches"][0]
# Get or create object
plus, created = Plusplus.get_or_create(
name=name.lower(),
defaults={'counter': 0}
)
plus.counter += 1
plus.save()
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
Python library to work with Jira APIs
$ pip install jira
Authentication
Create an API token
from jira import JIRA
url = 'https://jira.atlassian.com/'
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"
say(text)
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
Download quickstart.py from GitHub
Google Authorization is Complex
Run
quickstart.py
Select your Google account in Web browser
Click “Accept” button
Get
token.json
(finish!!)
$ python quickstart.py
Please visit this URL to authorize this application: https://accounts.google.com/o/oauth2/auth?....
Name, Major:
Alexandra, English
:
Issue template

Get Spreadsheet data
from google.oauth2.credentials import Credentials
from googleapiclient.discovery import build
SCOPES = ["https://www.googleapis.com/auth/spreadsheets.readonly"]
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 = datetime.date.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")
see: 2.1.4. Issues
Free from copying issues! 🎉


Account management of Google Workspace 👥
Account management of Google Workspace
Motivation
PyCon JP Association use
pycon.jp
domain with Google WorkspaceI 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
quickstart.py
Re-run
quickstart.py
Get new
token.json
Get user list
SCOPES = ["https://www.googleapis.com/auth/admin.directory.user"]
@app.message("user_list")
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] + "@pycon.jp",
"password": context["matches"][1],
"name": {
"givenName": context["matches"][2],
"familyName": context["matches"][3]
}}
service.users().insert(body=body).execute()
say(f"User {body['primaryEmail']} created")
Add user


I can forget Google Admin! 🎉
Security Issue 🔓
Anyone can run it
Run only Slack Admin 🔒
Not-Admin cannot run
Add
users:read
scope, use users.info API
@app.message("user_list")
def user_list(message, say, client):
# get user info: https://api.slack.com/methods/users.info
response = client.users_info(user=message["user"])
user = response.data["user"]
if not user["is_admin"]:
say("You are not an admin!")
return

Resolve a security issue 🎊
Summary
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")
say(result.text)
Thank you! 🙏

@takanory takanory takanory takanory