https://www.youtube.com/watch?v=SPTfmiYiuok
freeCodeCamp.orgが提供しているBot作成の動画を、自分でも試してみました。
今回作りたいBotは、このようなものです。
•通知を送る特定の人物を、テキストチャンネルからコマンドで追加・削除できる。
•ボイスチャンネルに誰かが入室すると、テキストチャンネルに、特定の人物に通知を送る。
•Magic; the Gatheringの裁定をWebページ上から取得し、それをDeepLで和訳したものを表示する。
ReplitからBotを作成することができます。
#DiscordのBot用のアカウントを作成する。
https://discord.com/developers/applications
Developer Portalに行き、Bot用のアカウントを作ります。
New Applicationをクリックします。
BotのNameを決めてから、Createを押します。
そうすると、BotのGeneral Information(一般情報)が得られます。
左のメニューのBotを押し、Add Botで、ボットに命を吹き込みます。
「この手続きは取り返しがつかない」と注意されます。熟慮の後に、Yes, do it!を選びます。
野生のBotが現れた!(A wild bot has appeared!)
続けて、Botの設定を進めていきます。
Botをサーバーに招くには、invite URLを作らねばなりません。
そのために、OAuth2をクリックします。
画面を下にスクロールし、Scopesを設定します。
Botを作っているので、botを選択します。
botを選択すると、下にURLが現れます。
Botに与える権限(Permissions)を設定します。
今回作りたいBotは、このようなものです。
•通知を送る特定の人物を、テキストチャンネルからコマンドで追加・削除できる。
•ボイスチャンネルに誰かが入室すると、テキストチャンネルに、特定の人物に通知を送る。
•Magic; the Gatheringの裁定を和訳したものを表示する。
適切な権限を与えていきます。
自作のサーバーで自作のBotを動かすなら、全部のPermissionsを与えてもいいんじゃないでしょうか。
BotのPermissionsを設定し終えたら、Scopes直下に表示されているURLをCopyします。
さあ、Botをサーバーに招待しましょう!
ブラウザから先ほどのURLに行き、好きなサーバーに追加します。
認証しましたの画面まで進むと、Botがサーバーに登録されます。
ただし、まだオフラインです。
#Botをreplitからオンラインにする
では、どうやってオンラインにするのでしょうか?
オンラインにするために今回は、replit上でpythonを動かします。
https://replit.com/
replitにサインイン後、ログインしましょう。
replitは、無料で使えるオンラインIDEです。
自分のPCをシャットダウンしても、replit上でBotを動かし続けられます。
Ξマーク、+New replの後、言語を選択します。
私はPythonを選択します。
有料のUpgradeをしない限り、replがpublicで作成されることに注意してください。
試しに何かコードを書いてみましょう。
真ん中にコードを書き、上のRunを押すと、右のConsoleに標準出力が表示されます。
replitは無料だとpublicになります。
そのため、keyやpasswordのようなものは、main.pyには書かず、環境変数として非公開にする必要があります。
左の錠のマークから、環境変数を作成します。
Discord Developer Portalから取得したTOKENを登録します。
キーの名前と、その値を入力して、Add new secretします。
Clientクラスは、Discordに接続するクライアント接続を表します。Clientクラスのインスタンスを作り、それをいじっていきます。
Botを作るためには、discordというライブラリを使います。
https://discordpy.readthedocs.io/ja/latest/index.html
# discordのボットに必要なライブラリ
import discord
# 環境変数を読み込むのに必要なライブラリ
import os
# 環境変数から、DISCODE_TOKENを取得
discord_token = os.environ['DISCORD_TOKEN']
# Clientのインスタンスを作成
client = discord.Client()
# ログインしたときに、コンソールにメッセージを表示する。
@client.event
async def on_ready():
print(f'{client.user}としてログインしました。')
# Discordに接続する。
client.run(discord_token)
ReplitのコンソールとDiscordの画面の両方でログインが確認できました。
#通知を送る特定の人物を、テキストチャンネルからコマンドで追加・削除する。
# メンション先を集合として用意します。
# このBotは、個人か使用する1つだけのサーバーで使用するのでこの方法で問題ありません。
# しかし、複数のサーバーでこのBotを使用すると、メンション先を1つの集合で管理しているので、不具合が出ます。
mention_to = set()
@client.event
async def on_message(message):
# globalを使って、関数の外の変数を書き込めるようにします。
global mention_to
# メッセージが「$add_me」から始まる場合
if message.content.startswith('$add_me'):
# message.author.id: メッセージを書いた人のID。int。
new_user = message.author.id
# メンション先を保存している集合に、new_userを加える。
mention_to.add(new_user)
#msg: ボットが書き込むメッセージ
msg = ""
# メンション先を、ボットが書き込むメッセージに加えます。
for user in mention_to:
# Discordのメンションは、<@!{ユーザーIDを表す整数}>で飛ばすことができます。
msg += f'<@!{user}> '
msg += f'{message.author}がメンション先に加わりました。'
# on_massage関数の引数として与えられたmessageのチャンネル(channel)に、msgを送る(send)
await message.channel.send(msg)
# メッセージが「$remove_me」から始まる場合
if message.content.startswith('$remove_me'):
old_user = message.author.id
if old_user in mention_to:
mention_to.remove(old_user)
msg = f'\nあなたをメンション先から削除しました。'
await message.channel.send(msg)
else:
await message.channel.send(f'あなたはメンション先として登録されていないため、メンション先から削除できません。')
#ボイスチャンネルに誰かが入室すると、テキストチャンネルに、特定の人物に通知を送る。
ボイスチャンネルの通知については、こちらの記事を参考にしました。
https://qiita.com/coolwind0202/items/34c8e3bee3680bde15ad
#Magic; the Gatheringの裁定をWebページ上から取得し、それをDeepLで和訳したものを表示する。
Scrythonではなくウェブスクレイピングの手法で、情報を取得します。
また、取得した情報をDeepL APIに送ります。
Magic; the Gatheringのカードの情報を取得するために、Scryfallが使われることがしばしばあります。
以下の内容は、scryfallのボットをすでにサーバーに招いていることを前提にしています。
https://scryfall.com/bots
Scryfallの情報をPythonで取得するためのライブラリとして、Scrythonがあります。
https://github.com/NandaScott/Scrython
今回も、Scrythonが利用できることが理想でした。
しかし、ReplitでScrythonをインポートしようとすると、Scrythonの依存先であるAsyncioのインポート時にエラーを起こします。そのため、今回はスクレイピングの手法を使って情報を取得します。
# BeautifulSoup: HTMLの操作を楽にするライブラリ
from bs4 import BeautifulSoup
# requests: 通信を行ってHTMLを取得するライブラリ
import requests
@client.event
async def on_message(message):
# ルーリングの取得
# メッセージがScryfall Botが示すScryfallのURLであった場合に、情報の取得を開始します。
# 今回はmessage.contentだけを条件としているものの、message.authorを条件に加えたほうがより厳密です。
if message.content.startswith('https://scryfall.com') and message.content.endswith('utm_source=discord'):
try:
url = message.content
# urlの内容を、requestsを使用してrとして読み込みます。
r = requests.get(url)
# 読み込んだ内容を、扱いやすいように、BeautifulSoupのインスタンスにします。
soup = BeautifulSoup(r.text,'html.parser')
# find_allメソッドは、HTMLタグのAttributeが条件に合う要素を探してくれます。
# 結果は、見つかった要素のリストです。
# 今回は、タグ内に「class = "rulings-item"」と書かれているものを探します。
# class ではなく class_ とアンダーバーが付いているのは、pythonがすでに使用しているclassという語とぶつかり合わないためです。
td_list = soup.find_all(class_ = "rulings-item")
# find_allとちがい、findは、見つかった要素そのものを返します。
# .textは、その要素のtextを返します。
# たとえば「<h1 class = "card-text-title">Grizzly Bears</h1>」のtextは、「Grizzly Bears」です。
cardname = soup.find(class_="card-text-title").text
# td_listというリストとして得ている内容を、textという名前のstrにします。
text = ''
for item in td_list:
text += item.text
#replitに登録しているDEEPL_TOKENを引き出します。
DEEPL_TOKEN = os.environ['DEEPL_TOKEN']
# DeepL APIに与える値
# 「textを、原文(source_lang)の英語(EN)から、目標言語(target_lang)の日本語(JA)に翻訳してもらいたい」ということを言っています。
deepl_params = {
"auth_key": DEEPL_TOKEN,
"text": text,
"source_lang": 'EN',
"target_lang": 'JA'
}
# requests.postを使って、DeepLにお願いをdataとして送ります。
deepl_request = requests.post("https://api-free.deepl.com/v2/translate", data=deepl_params)
# DeepLからかえって来たjsonの中から、翻訳された内容を取り出します。
deepl_result = deepl_request.json()["translations"][0]["text"]
# 翻訳された内容をDiscordに書き込みます。書き込み先は引数messageが書かれたのと同じchannelです。
await message.channel.send(f'{cardname} \n【裁定】:\n' + deepl_result)
# 原文をDiscordに書き込みます。書き込み先は引数messageが書かれたのと同じchannelです。
await message.channel.send('【原文】:\n' + text)
except:
await message.channel.send(f'カードが見つかりません。')
Scryfallのコマンドを使ってDiscordに書き込むと、Scryfall botが該当するカードを表示します。
ここで、今回のようなBotを作る際に、Webページ上で得たい要素を我々人間が探す方法について説明します。
Google Chromeで要素を探したいページに行き、CTRL + SHIFT + Cを押すと、開発者ツールが表示されます。
この状態で、Botに取得させたい要素をクリックすると、その部分のHTMLがクローズアップされます。
取得したい要素のclassやidの法則を根性で見つけ出してください。
今回は、欲しい要素が classで抜けそうだったので、「td_list = soup.find_all(class_ = "rulings-item")」や「cardname = soup.find(class_="card-text-title").text」とコーディングしています。
DeepLをBotで無料で使用するために、DeepL API Freeのアカウントを作ります。
アカウントの画面を下にスクロールすると、「DeepL APIで使用する認証キー」を取得できます。
取得した「DeepL APIで使用する認証キー」についても、左の錠のマークから環境変数を作成します。
#Botを延命する。
一定時間リクエストがないと、このReplitで作ったBotはスリープしてしまいます。
タブを閉じても動き続けるように、別の巡回ロボットがReplitのbotを見張るようにします。
まず、見張り先のURLを用意します。
Add fileから「keep_alive.py」というファイルを作り、以下のように書きます。
from flask import Flask
from threading import Thread
app = Flask('')
@app.route('/')
def home():
return 'Hello. I am alive!'
def run():
app.run(host = '0.0.0.0', port = 8080)
def keep_alive():
t = Thread(target = run)
t.start()
また、main.pyにも以下のように書き、keep_aliveを実行できるようにします。
from keep_alive import keep_alive
keep_alive()
このようにして上のRunボタンを押すと、画面右上にURLが表示されます。
このURLに何者かが訪れるたびに、Botは「Hello, I am alive!」と表示します。
このURLを訪れる何者かを用意するために、 https://uptimerobot.com/ を使用します。
UptimeRobotにログイン後、+Add New Monitorをクリックします。
Monitor Type: HTTP(s)
Friendly Name:任意
URL: 先ほどのURL
これらを入力し、右下のCreate Monitorをクリックします。
#終わり
これで、Pythonで無料のBotを作った話は終わりです。
以下が、コードの全文です。
import discord
# 環境変数を読み込むのに必要なライブラリ
import os
# gathererのルーリングはScrythonから入手できますが、scrythonをインストールすると
# 現状のreplitではasyncioのインストールでエラーを起こすため、
# requestとbs4を使用して
# ルーリングを取得します。
import requests
from bs4 import BeautifulSoup
import re
# ボットの延命
from keep_alive import keep_alive
# 環境変数から、DISCODE_TOKENを取得
discord_token = os.environ['DISCORD_TOKEN']
# Clientのインスタンスを作成
client = discord.Client()
# ログインしたときに、コンソールにメッセージを表示する。
@client.event
async def on_ready():
print(f'{client.user}としてログインしました')
mention_to = set()
text_channel = ""
# ボイスチャンネルの通知については、こちらの記事を参考にしています。
# https://qiita.com/coolwind0202/items/34c8e3bee3680bde15ad
@client.event
async def on_vc_start(member,channel):
msg = ''
for user in mention_to:
msg += f'<@!{user}> '
msg += f"\n{member.name}が{channel.name}でボイスチャットを開始しました。"
print(text_channel, type(text_channel))
sending_channel = client.get_channel(text_channel)
try:
await sending_channel.send(msg)
except:
print('チャンネルの設定がされていません。')
@client.event
async def on_vc_end(member,channel):
msg = ''
for user in mention_to:
msg += f'<@!{user}> '
msg += f"\n{member.name}が{channel.name}のボイスチャットを終了しました"
sending_channel = client.get_channel(text_channel)
try:
await sending_channel.send(msg)
except:
print('チャンネルの設定がされていません。')
@client.event
async def on_voice_state_update(member,before,after):
if before.channel != after.channel:
# before.channelとafter.channelが異なるなら入退室
if after.channel and len(after.channel.members) == 1:
# もし、ボイスチャットが開始されたら
client.dispatch("vc_start",member,after.channel) #発火!
if before.channel and len(before.channel.members) == 0:
# もし、ボイスチャットが終了したら
client.dispatch("vc_end",member,before.channel) #発火!
@client.event
async def on_message(message):
global mention_to
global text_channel
if message.content.startswith('$add_me'):
print('message reveived')
new_user = message.author.id
mention_to.add(new_user)
msg = ""
for user in mention_to:
msg += f'<@!{user}> '
msg += f'{message.author}がメンション先に加わりました。'
await message.channel.send(msg)
if message.content.startswith('$remove_me'):
print('message reveived')
old_user = message.author.id
if old_user in mention_to:
mention_to.remove(old_user)
msg = f'\nあなたをメンション先から削除しました。'
await message.channel.send(msg)
else:
await message.channel.send(f'あなたはメンション先として登録されていないため、メンション先から削除できません。')
if message.content.startswith('$change_channel_id'):
text_channel = message.channel.id
msg = ""
for user in mention_to:
msg += f'<@!{user}> '
msg += f'通話の通知先をこのチャンネルに変更しました。'
await message.channel.send(msg)
'ルーリングの取得'
if message.content.startswith('https://scryfall.com') and message.content.endswith('utm_source=discord'):
try:
url = message.content
r = requests.get(url)
soup = BeautifulSoup(r.text,'html.parser')
td_list = soup.find_all(class_ = "rulings-item")
print(td_list, '=td_list')
cardname = soup.find(class_="card-text-title").text
print(cardname, '= cardname')
text = ''
for item in td_list:
print(item, '= item')
text += item.text
print(text, '=text')
DEEPL_TOKEN = os.environ['DEEPL_TOKEN']
# DeepL APIに与える値
print('133')
deepl_params = {
"auth_key": DEEPL_TOKEN,
"text": text,
"source_lang": 'EN',
"target_lang": 'JA'
}
deepl_request = requests.post("https://api-free.deepl.com/v2/translate", data=deepl_params)
print(143)
deepl_result = deepl_request.json()["translations"][0]["text"]
print(deepl_result, '=deepl_result')
await message.channel.send(f'{cardname} \n【裁定】:\n' + deepl_result)
print(150)
await message.channel.send('【原文】:\n' + text)
except:
await message.channel.send(f'カードが見つかりません。')
keep_alive()
# Discordに接続する。
client.run(discord_token)