LoginSignup
0
4

More than 1 year has passed since last update.

DiscordのBotをPythonで無料で作った話

Posted at

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用のアカウントを作ります。

NewApplication.png

New Applicationをクリックします。

NameCreate.png

BotのNameを決めてから、Createを押します。

GenealInformatino.png

そうすると、BotのGeneral Information(一般情報)が得られます。

addbot.png

左のメニューのBotを押し、Add Botで、ボットに命を吹き込みます。

irrevocable.png

「この手続きは取り返しがつかない」と注意されます。熟慮の後に、Yes, do it!を選びます。

野生のボットが現れた.png

野生のBotが現れた!(A wild bot has appeared!)

続けて、Botの設定を進めていきます。

Botをサーバーに招くには、invite URLを作らねばなりません。
そのために、OAuth2をクリックします。
OAuth2.png

scope_bot.png

画面を下にスクロールし、Scopesを設定します。
Botを作っているので、botを選択します。
botを選択すると、下にURLが現れます。

BOTS_PERMISSIONS.png

Botに与える権限(Permissions)を設定します。
今回作りたいBotは、このようなものです。
•通知を送る特定の人物を、テキストチャンネルからコマンドで追加・削除できる。
•ボイスチャンネルに誰かが入室すると、テキストチャンネルに、特定の人物に通知を送る。
•Magic; the Gatheringの裁定を和訳したものを表示する。
適切な権限を与えていきます。
自作のサーバーで自作のBotを動かすなら、全部のPermissionsを与えてもいいんじゃないでしょうか。

URL_copy.png

BotのPermissionsを設定し終えたら、Scopes直下に表示されているURLをCopyします。

INVITE_BPT.png
さあ、Botをサーバーに招待しましょう!
ブラウザから先ほどのURLに行き、好きなサーバーに追加します。

認証しました.png

認証しましたの画面まで進むと、Botがサーバーに登録されます。
ただし、まだオフラインです。

オフライン.png

Botをreplitからオンラインにする

では、どうやってオンラインにするのでしょうか?
オンラインにするために今回は、replit上でpythonを動かします。

replit.png

https://replit.com/
replitにサインイン後、ログインしましょう。
replitは、無料で使えるオンラインIDEです。
自分のPCをシャットダウンしても、replit上でBotを動かし続けられます。

NewRepl.png

Ξマーク、+New replの後、言語を選択します。
私はPythonを選択します。
有料のUpgradeをしない限り、replがpublicで作成されることに注意してください。

NamedAndCreate.png
名前を入力してから、Create replを押します。

IDEOPENED.png
IDEの画面が開きました。

testcode.png
試しに何かコードを書いてみましょう。
真ん中にコードを書き、上のRunを押すと、右のConsoleに標準出力が表示されます。

secrets.png
replitは無料だとpublicになります。
そのため、keyやpasswordのようなものは、main.pyには書かず、環境変数として非公開にする必要があります。

左の錠のマークから、環境変数を作成します。

TOKEN.png

Discord Developer Portalから取得したTOKENを登録します。

add_new_secret.png
キーの名前と、その値を入力して、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)

login_on_repl.png
login_on_discord.png
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'あなたはメンション先として登録されていないため、メンション先から削除できません。')

add_remove_me.png
このようになります。

ボイスチャンネルに誰かが入室すると、テキストチャンネルに、特定の人物に通知を送る。

ボイスチャンネルの通知については、こちらの記事を参考にしました。
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'カードが見つかりません。')


Fireball.png
Scryfallのコマンドを使ってDiscordに書き込むと、Scryfall botが該当するカードを表示します。

開発者ツール.png
ここで、今回のような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」とコーディングしています。

DeepLAPI_FREE.png
DeepLをBotで無料で使用するために、DeepL API Freeのアカウントを作ります。

DeepL_Key.png

アカウントの画面を下にスクロールすると、「DeepL APIで使用する認証キー」を取得できます。

secrets_deepL.png
取得した「DeepL APIで使用する認証キー」についても、左の錠のマークから環境変数を作成します。

ensnaring_bridge.png
このようになります。

Botを延命する。

一定時間リクエストがないと、このReplitで作ったBotはスリープしてしまいます。
タブを閉じても動き続けるように、別の巡回ロボットがReplitのbotを見張るようにします。
まず、見張り先のURLを用意します。

keep_alive.png

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()

URL.png

このようにして上のRunボタンを押すと、画面右上にURLが表示されます。
このURLに何者かが訪れるたびに、Botは「Hello, I am alive!」と表示します。

このURLを訪れる何者かを用意するために、 https://uptimerobot.com/ を使用します。

uptimerobot.png
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)
0
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
4