【初心者向け】Discord.pyを使って社内ツールを実装してみる#01(Backlogの課題キーをリンクにする)
BacklogとDisordを連携する
Nulab社が提供する"Typetalk"のサービス終了が発表されたことをきっかけに、弊社ではDiscordへの移行を行いました。
Nulab社が提供する"Backlog"は、同社が提供する"Typetalk"との連携がすばらしく、非常に快適でした。例えば、Backlogの課題キーをリンクにする機能がついていました。
Backlogの課題キーとは、こういうやつです🐈
CAT-222 猫に餌をやる
厳密には、"CAT-222"の部分ですが、コピペするボタンをクリックすると{課題キー+件名}でコピーされるので、ここでの定義は件名までを含むものとします。
当然といえば当然ですが、Discordにはこの機能がついていません。
そこで今回は、この「課題キーをリンクにする」機能を持ったbotを作成していきます。ただ、元のメッセージをbotが編集することはDiscordのポリシー的に禁じられているので、botが元のメッセージの「中身」を編集して新しいメッセージを送信し、元のメッセージを削除するということをします。
📌アイコンがbotのアイコンになってしまうのでわかりにくい場合は、webhookを使うと成りすます 本人が送信したように見せることもできます。ただし、webhookはチャンネル依存のため、保守性の観点からおすすめはしません。
まずは最低限のbotから
複雑な処理を含める前に、まずは基礎的なbotを作成します。
Discord.devでの操作やinstallationは生成AIに聞けば教えてくれるので、省略します。
discord.pyを使ったdiscord-botのつくり方を初心者にもわかるように教えて!
みたいな感じで聞いてみると教えてくれます。いちおう、intentの設定をONにするところだけ忘れないようにしてください(なぜかデフォルトではOFFになっています)。
以下が基本的なbotの枠組みになります。誰かが"hello!"と送ったら"goodbye!"と送るビートルズかぶれのbotです。
# main.py
import discord
import os
from dotenv import load_dotenv
load_dotenv()
# このあたりはほとんど定型文みたいなもの
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
# ログインしたらログ出力する
@client.event
async def on_ready():
print("Bot is logged in and ready!")
# メッセージを受信したときの対応
@client.event
async def on_message(message):
if message.content == "hello!":
await message.channel.send("goodbye!")
TOKEN = os.getenv("DISCORD_TOKEN")
client.run(TOKEN)
ソースにTOKENを直に貼るのはよくないので、.envファイルに保存します(後にwebserverに上げた際に事故るのを防ぐため)。
# .env
DISCORD_TOKEN = "ここにボットのトークンを保存"
課題キーを分解する
課題キーは、
CAT-222 猫に餌をやる
狭義の課題キー+件名で構成されているとお話しました。しかし、実はもっと細かくすることもできます。そう、"CAT"はプロジェクトキーです。
課題キー(広義)は、プロジェクトキー+"-"+数字+任意の文字列ととらえることができます。プロジェクトキーで検索し、さらにプロジェクトキーらしく文字列の後に"-"や数字が来ることを指定すれば、かなりエラーは起こりにくくなります。
プロジェクトキーを保存する
検索ワードとして利用するプロジェクトキーは、あらかじめ手動で保存しておく必要があります。そこだけちょっと不便ですが、仕方ない部分ではあると思います(APIで取得できるのかも?)。
以下の手順で、プロジェクトキーをcsvに保存します。
(1)Backlogのダッシュボードで、組織のプロジェクトをすべてコピペ
(2)main.pyや.envを保存したのと同じディレクトリにproject_keys.csvを作成
(3)頭にproject_keysと書いて、その下にプロジェクトキーを列挙(下図参照)
project_keys
CAT
DOG
BIRD
SNAKE
DINOSAUR
csv = コンマ区切りですが、vscodeで表示すると1列のデータは","なしで表示してくれました(だから何ということでもない)。
プロジェクトキーを読み込む
プロジェクトキーで検索するために、csvファイルから読み込む関数を定義します。
def load_project_keys(csv_file):
PROJECTKEYS = pd.read_csv(csv_file)
return PROJECTKEYS["project_name"].tolist()
プロジェクトキーを検知してリンクを生成
メッセージが来たときの処理を記述します。全体が見えたほうがわかりやすいので、全体をお見せします。
@client.event
async def on_message(message):
# bot自身が送信したメッセージには反応しない
if message.author == client.user:
return
# 万が一元のメッセージに型崩れしたmarkdownがあった場合の暴走を止める
if "linkgenerator" in message.content:
return
# 暴走したときに"$stop"で強制ログアウト
if message.content == "$stop":
await message.channel.send("Bot is logging out...")
await client.close()
return
new_content = message.content # 元メッセージをリサイクル
sender = message.author.display_name # メッセージ送信者のサーバー表示名を取得
# 各プロジェクトキーを検知してリンクを生成
for project_key in project_keys:
pattern = re.compile(
rf"({project_key}-\d+)\s+(.+?)(?=\s|$)"
) # 課題キー+件名のフォーマット
new_content = pattern.sub(
lambda m: f"[{m.group(0)}](https://YOUR_ORGNIZATION_NAME_HERE.backlog.com/view/{m.group(1)})",
new_content,
) # 課題キーが存在する位置を基準として、"m.group"で前後を取り扱っています。
# リンクを生成する箇所があった場合(元のメッセージに変更があった場合)、新しいメッセージを送信して元のメッセージを削除
if new_content != message.content:
reply_content = f"{new_content}\nedited by linkgenerator as **{sender}**"
await message.delete()
await message.channel.send(reply_content)
分解してみると、一つ一つの処理はそこまで複雑ではないことがわかると思います。正規表現に関する部分などは、すでにさまざまなサイトで紹介されていますので、「Python 正規表現」などと検索してみるとよいかもしれません。
特筆すべき点があるとすれば、この部分でしょうか。
# 万が一元のメッセージに型崩れしたmarkdownがあった場合の暴走を止める
if "linkgenerator" in message.content:
return
if new_content != message.content:
reply_content = f"{new_content}\nedited by linkgenerator as **{sender}**"
botが送信するメッセージには、"linkgenerator"が送信したメッセージであること、元の送信者が"sender"であることが書かれています。
さきほど、Discordでは成りすましが禁止されていると述べましたが、ポリシーの問題だけでなく、誰が送信したかわからないと会話にならないので(笑)、署名は重要になります。
また、ついでにbotが送信した情報であることを明示することで、万が一botが自分のメッセージに反応してしまった場合のトラブル(永遠にリンク化の形式を吐き続ける)を防ぐことができます。
もちろん、
if message.author == client.user:
return
をやっているので、自分自身のメッセージに反応することは基本的にないはずですが、誰かがメッセージをそのままコピーして貼った場合など、念には念を入れた措置です。
全部見たいでしょ?
いいから全部見せてよ!というあなたのために、ソース全文を貼っておきます。importするライブラリ系はめんどくさくて 書いていないですしね。
一番下のコードをコピペして、ご自分の環境で実行してみてください。「discord.py」、「requests」、「pandas」、「python-dotenv」をpip installするのと、.envにトークンを記述するのも忘れずに。
※たぶん大丈夫ですが、動作は保証いたしかねます。
# main.py
import discord
import re
import requests
import pandas as pd
from dotenv import load_dotenv
import os
load_dotenv()
discord_token = os.getenv("TOKEN")
intents = discord.Intents.default()
intents.message_content = True
client = discord.Client(intents=intents)
# CSVファイルからプロジェクトキーを読み込む関数
def load_project_keys(csv_file):
df = pd.read_csv(csv_file)
return df["project_key"].tolist()
project_keys = load_project_keys("project_keys.csv")
@client.event
async def on_ready():
print(f"logged in as {client.user}")
@client.event
async def on_message(message):
if message.author == client.user:
return
if "linkgenerator" in message.content:
return
if message.content == "$stop":
await message.channel.send("Bot is logging out...")
await client.close()
return
new_content = message.content
sender = message.author.display_name
avatar_url = message.author.avatar.url if message.author.avatar else None
for project_key in project_keys:
pattern = re.compile(
rf"({project_key}-\d+)\s+(.+?)(?=\s|$)"
)
new_content = pattern.sub(
lambda m: f"[{m.group(0)}](https://YOUR_ORGANIZATION_NAME.backlog.com/view/{m.group(1)})",
new_content,
)
if new_content != message.content:
reply_content = f"{new_content}\nedited by linkgenerator as **{sender}**"
# response = requests.post(webhook_url, json=data)
# if response.status_code != 204:
# print(f"Error sending message through webhook: {response.status_code}, {response.text}")
await message.delete()
await message.channel.send(reply_content)
client.run(discord_token)
おわりに
今回ご紹介したソースには、問題がある場合もあります。うまくいかないよ!こうしたらもっとよくなるよ!というアドバイスがあれば、どしどしお寄せください。
みなさまのお役に立てていれば幸いです。
Discussion