Automate the Boring Stuff with Slackbot (ver. 2)
Takanori Suzuki
PyCon US 2022 / 2022 Apr 29
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 🐦 👍
#pyconus2022
/ @takanory
Slide 💻
Why ver. 2 in the title?
Back to 2020
PyCon US 2020 went online
![../_images/pyconus2020.png](../_images/pyconus2020.png)
Came back at 2022
Talk updated with latest information
I am very happy!!
Thanks to PyCon US staff!!
Who am I? 👤
Takanori Suzuki / 鈴木 たかのり ( @takanory)
Vice Chair of PyCon JP Association
Director of BeProud Inc.
Python Boot Camp, Python mini Hack-a-thon, Python Bouldering Club
![../_images/sokidan-square.jpg](../_images/sokidan-square.jpg)
PyCon JP 2022 🇯🇵
Date: 2022 Oct 14 (Fri) - 16 (Sun)
Venue: Tokyo, Japan (in-person)
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
Programmer is Lazy
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 Slack
To do everything in Slack
![../_images/slack1.png](../_images/slack1.png)
You can create interactive bot
![../_images/bot-result1.png](../_images/bot-result1.png)
![../_images/bot-result2.png](../_images/bot-result2.png)
Simple integration with Incoming Webhooks 🪝
System overview
![../_images/diagram-webhook.png](../_images/diagram-webhook.png)
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
![../_images/create-webhook1-1.png](../_images/create-webhook1-1.png)
![../_images/create-webhook1-2.png](../_images/create-webhook1-2.png)
Enter name and choose workspace
![../_images/create-webhook2.png](../_images/create-webhook2.png)
Set app icon (optional)
![../_images/create-webhook3.png](../_images/create-webhook3.png)
2. Activate Incoming Webhooks
![../_images/create-webhook4-1.png](../_images/create-webhook4-1.png)
3. Add Webhook to Workspace
Click “Add New Webhook to Workspace”
![../_images/create-webhook4-2.png](../_images/create-webhook4-2.png)
Choose channel → Click “Allow”
![../_images/create-webhook5.png](../_images/create-webhook5.png)
Get Webhook URL:
https://hooks.slack.com/...
![../_images/create-webhook6.png](../_images/create-webhook6.png)
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...
![../_images/webhook-curl.png](../_images/webhook-curl.png)
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)
![../_images/webhook-python.png](../_images/webhook-python.png)
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)
![../_images/webhook-requests.png](../_images/webhook-requests.png)
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!")
![../_images/webhook-slacksdk.png](../_images/webhook-slacksdk.png)
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:")
![../_images/webhook-formatting.gif](../_images/webhook-formatting.gif)
Block Kit 🧱
Block Kit
![../_images/block-kit.png](../_images/block-kit.png)
see: Block Kit
Example of Block Kit
blocks = [{
"type": "section",
"text": {
"text": "*THANK YOU* for coming to my talk !:tada: Please give me *feedback* about this talk :bow:",
"type": "mrkdwn"
},
"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)
![../_images/webhook-blocks.png](../_images/webhook-blocks.png)
Block Kit Builder
![../_images/block-kit-builder.gif](../_images/block-kit-builder.gif)
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
![../_images/diagram-eventsapi.png](../_images/diagram-eventsapi.png)
Socket Mode
![../_images/diagram-socketmode.png](../_images/diagram-socketmode.png)
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
![../_images/create-webhook1-1.png](../_images/create-webhook1-1.png)
![../_images/create-webhook1-2.png](../_images/create-webhook1-2.png)
Enter name and choose workspace
![../_images/create-bot2.png](../_images/create-bot2.png)
Set app icon (optional)
![../_images/create-bot3.png](../_images/create-bot3.png)
2. Enable Socket Mode
Select “Socket Mode” → Turn toggle on
![../_images/create-bot4.png](../_images/create-bot4.png)
Enter token name → Click “Generate”
![../_images/create-bot5.png](../_images/create-bot5.png)
Get app-level token:
xapp-...
![../_images/create-bot6.png](../_images/create-bot6.png)
3. Subscribe bot event
Select “Event Subscriptions” → Turn toggle on
![../_images/create-bot3-1.png](../_images/create-bot3-1.png)
Add “message.channels” to bot events
![../_images/create-bot3-2-1.png](../_images/create-bot3-2-1.png)
![../_images/create-bot3-2-2.png](../_images/create-bot3-2-2.png)
4. Add Bot Token Scopes
Select “OAuth & Permissions”
![../_images/create-bot7.png](../_images/create-bot7.png)
Click “Add on OAuth Scope”
![../_images/create-bot8.png](../_images/create-bot8.png)
Add “chat:write” to Bot Token Scopes
![../_images/create-bot9.png](../_images/create-bot9.png)
![../_images/create-bot10.png](../_images/create-bot10.png)
5. Install App to Workspace
Select “Install App” → Click “Install to Workspace”
![../_images/create-bot11.png](../_images/create-bot11.png)
Switch OAuth screen → Click “Allow” button
![../_images/create-bot12.png](../_images/create-bot12.png)
Get Bot Token:
xoxb-...
![../_images/create-bot13.png](../_images/create-bot13.png)
Invite bot user to channels
![../_images/invite-bot.png](../_images/invite-bot.png)
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
![../_images/get-app-manifest.png](../_images/get-app-manifest.png)
Create new app with App Manifest
Select “From an app manifest”
Select workspace → Click “Next”
![../_images/app-manifest1.png](../_images/app-manifest1.png)
![../_images/app-manifest2.png](../_images/app-manifest2.png)
Enter app manifest YAML
![../_images/app-manifest3.png](../_images/app-manifest3.png)
Review app summary → Click “Create”
![../_images/app-manifest4.png](../_images/app-manifest4.png)
![../_images/app-manifest5.png](../_images/app-manifest5.png)
![../_images/app-manifest6.png](../_images/app-manifest6.png)
Install App to Workspace
![../_images/app-manifest7.png](../_images/app-manifest7.png)
Generate App-Level Token
![../_images/app-manifest8.png](../_images/app-manifest8.png)
![../_images/app-manifest9.png](../_images/app-manifest9.png)
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.10 -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!
![../_images/bot-hi.png](../_images/bot-hi.png)
I can interact with the bot ! 🎉
![../_images/bot-hi.png](../_images/bot-hi.png)
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:")
![../_images/bot-decolator.png](../_images/bot-decolator.png)
mention
# mention
@app.message("morning")
def handle_morning_message(message, say):
user = message["user"] # get user ID
say(f"Good morning <@{user}>!")
![../_images/bot-mention.png](../_images/bot-mention.png)
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))
![../_images/bot-choice.png](../_images/bot-choice.png)
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)
![../_images/bot-beers.png](../_images/bot-beers.png)
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)
![../_images/bot-followme.png](../_images/bot-followme.png)
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
![../_images/bot-logging.png](../_images/bot-logging.png)
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:
message posted to public 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
![../_images/bot-private-cannot-view.png](../_images/bot-private-cannot-view.png)
Add Events and Scopes for private channels
Select “Event Subscriptions” → Click “Add Bot User Event”
Add message.groups event→ Click “Save Changes”
![../_images/add-events-and-scopes1.png](../_images/add-events-and-scopes1.png)
Add Events and Scopes for private channels
Select “OAuth & Permissions”
groups:history scope is automatically added
![../_images/add-events-and-scopes2.png](../_images/add-events-and-scopes2.png)
Add Events and Scopes for private channels
Reinstall app to workspace
![../_images/add-events-and-scopes3.png](../_images/add-events-and-scopes3.png)
![../_images/add-events-and-scopes4.png](../_images/add-events-and-scopes4.png)
Add Events and Scopes for private channels
Bot can read/write messages in private channel
![../_images/add-events-and-scopes6.png](../_images/add-events-and-scopes6.png)
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:")
![../_images/event-member-joined.png](../_images/event-member-joined.png)
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",
)
![../_images/scope-reactions-write.png](../_images/scope-reactions-write.png)
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
![../_images/diagram-sympy.png](../_images/diagram-sympy.png)
about SymPy
![https://www.sympy.org/static/images/logo.png](https://www.sympy.org/static/images/logo.png)
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 the formula
if result.is_Integer:
answer = int(result) # Convert to integer value
else:
answer = float(result) # Convert to float value
say(f"{answer:,}")
except SympifyError:
pass
Slack as a calculator !!
![../_images/case-sympy.png](../_images/case-sympy.png)
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
![../_images/diagram-peewee.png](../_images/diagram-peewee.png)
about Peewee
![https://docs.peewee-orm.com/en/latest/_images/peewee3-logo.png](https://docs.peewee-orm.com/en/latest/_images/peewee3-logo.png)
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!
![../_images/case-peewee.png](../_images/case-peewee.png)
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
![../_images/diagram-jira.png](../_images/diagram-jira.png)
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!
![../_images/bot-jira.png](../_images/bot-jira.png)
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
![../_images/diagram-template.png](../_images/diagram-template.png)
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
![../_images/bot-issue-template.png](../_images/bot-issue-template.png)
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
![../_images/bot-sheet1.png](../_images/bot-sheet1.png)
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!
![../_images/bot-sheet2.png](../_images/bot-sheet2.png)
![../_images/bot-sheet3.png](../_images/bot-sheet3.png)
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
![../_images/diagram-directory.png](../_images/diagram-directory.png)
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
![../_images/bot-user-list.png](../_images/bot-user-list.png)
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
![../_images/bot-user-add.png](../_images/bot-user-add.png)
![../_images/bot-user-add2.png](../_images/bot-user-add2.png)
I can forget Google Admin!
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! 🙏
![../_images/bot-translate.png](../_images/bot-translate.png)
github.com/takanory/slides/tree/master/slides/20220429pyconus/code
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! 🙏
![../_images/bot-translate.png](../_images/bot-translate.png)
github.com/takanory/slides/tree/master/slides/20220429pyconus/code