のうない あうとぷっと。
プログラミング

PythonでDiscord BOTを作成した話

どうも、わいつぼいです。

イケハヤさんが開設して今力を入れている

ブロガーズギルドfreeに参加しています。

ブロガーズギルドの中には

「部活動」というコミュニティ内コミュニティがあるんですが

その中で「プログラミング部」というところに所属してます。

(所属といっても制限はなく、好きな時に好きなところに参加する感じです。)

今回、借金部という部活から

借金管理するソフトを作って欲しいと依頼があったので、

ちょっと手を上げて作ってみました。

要求仕様

要求仕様はまずコーダさんより、

コーダ-2018/07/09
おはようございます!

借金部から質問があり参りました。

みんなの借金総額が毎月の返済により減っていくカウンターのようなものがあったら面白いよね!って話をしたのですが、そのようなものは実際に作れるものなのでしょうか?

また、作ろうと思ったらどれぐらい費用としてかかるものなのでしょうか?

どなたか分かる方いらっしゃったら教えて頂けると幸いで> す!!!

わいつぼいは、Web上のアプリを想定して回答しました。

わいつぼい-2018/07/09

みんなの借金総額が毎月の返済により減っていくカウンターのようなものがあったら面白いよね!って話をしたのですが、そのようなものは実際に作れるものなのでしょうか?
はい、作れます。どういった機能にするかにもよるのですが、
例えば
・初回登録時に借り入れが額を入力する(ユーザ名と総額)
・トップ画面にはみんなの借り入れ総額が表示される
・個人の情報は出さないようにする。(当たり前ですが)
・個人用の金額管理画面があって、返済日には返済金額を手動で入力する。
・トップ画面の金額が減る。
みたいなのとかでしょうか?思いつきで申し訳ないですが・・・^^;

また、作ろうと思ったらどれぐらい費用としてかかるものなのでしょうか?
上記の機能次第になりますが、
予算から実装できるものを決めるという方法もあります。
フリーランスとかではないので、相場はちょっとわからないですが(ごめんなさい。

あとは誰が運用するか(借金部が運用するか、プログラミング部が運用するか、製作者個人が運用するか)によって
維持管理費が決まっていくかと。ざっくりいうとサーバ代とかドメイン代でしょうか。

上記で回答になるかわかりませんが、いかがでしょうか?
わからないところがあればレスくださいーい

と、回答しました。

そのあとりゅーせいさんからコメントがあり、

りゅーせい/借金フリーター-2018/07/09
借金部からきました!
ウェブ上だと、リンク飛ばないといけなくてちょっと不便なので、Discord でbotを組んだら使いやすいかなーと思ったんですけど、可能ですかね?

と回答があり、Webアプリケーションではなく、

Discord上のBOTとして機能実装していくことに。

自分がよく使っているPython + MySQLで作りました。

実装機能

上記要求仕様を元に

・新規借金データ登録
・借金返済
・借金借入
・自分の情報確認
・登録者全体の情報確認

の5つのコマンドを実装することにしました。

追加でランキング機能もリクエストされましたが、

これはまた別途。。。(まだちゃんと動かせてないのでもう少々お待ちください・・・

コーディング/プログラム

Discord Botの作りかたはあまりわかってなかったので

「python discord bot」らへんのキーワードでググって

出てきた上位5件くらいを見て書きました。

書いたプログラム(gitで公開しているプログラム)はこんな感じ

# -*- coding: utf-8 -*-
#################################
# loan_counter_bot
# 
# Version: 0.1
# Copyright (c) ytsuboi0322
# twitter: https://twitter/ytsuboi0322
# 
#################################
import discord
import asyncio
import MySQLdb

client = discord.Client()
# 試験用トークンID
token = "***********" # ここにDiscordで発行するIDを入れる

#################################
# Usage文
#################################
Usage="""
 コマンド
   ?loanset   : 初期登録
   ?repay     : 返済額入力
   ?reset     : 自分の登録借入額を修正
   ?add       : 借入額追加
   ?total     : 総ユーザ借入金額を表示
   ?info      : 情報を表示
   ?loanrank  : 借入金額TOP10ユーザを表示 # 未実装
   ?repayrank : 返済金額TOP10ユーザを表示 # 未実装
 usage
   <コマンド>△<数値>
   △は半角スペース
   例)
     ?loanset 100000
"""
#################################
# SQL文
#################################
# 新規登録
createSQL= \
    "INSERT INTO \
        loan_user_data ( \
            username,\
            loan,\
            datecreated,\
            updatedate\
        ) \
        VALUES( \
            %s,\
            %s,\
            now(),\
            now() \
        )"
# データ更新
updateSQL= \
    "UPDATE \
        loan_user_data \
    SET \
        loan = %s ,\
        updatedate = now() \
    WHERE \
        username = %s "
# 返済額テーブルにデータを登録する
repaySQL=\
    "INSERT INTO  \
        repayment_data (\
        username , \
        repayment , \
        repaymentdate ) \
    VALUES( \
        %s  ,\
        %s  ,\
        now() \
    ) "
# 個人の登録データを取得する
readPersonSQL= \
    'SELECT \
        username ,\
        loan \
    FROM \
        loan_user_data \
    WHERE \
        username = %s '
# 総借入額を取得
readAllloanSQL= \
    "SELECT \
        sum(loan) \
    FROM \
        loan_user_data "
# 総登録人数を取得
readPersonCountSQL= \
    "SELECT \
        count(username) \
    FROM \
        loan_user_data "
# 返済総額を取得
repayAllSQL = \
    "SELECT \
        username ,\
        sum(loan) \
    FROM \
        loan_user_data \
    WHERE \
        username = %s "

#################################
# ログイン時のアクション
#################################
@client.event
async def on_ready():
    print('Logged in as')
    print(client.user.name)
    print(client.user.id)
    print('------')

#################################
# ログキャッチ処理
#################################
@client.event
async def on_message(message):
    # 任意のチャンネル名のみでしか呟かない
    send_channel = [channel for channel in client.get_all_channels() if channel.name == 'チャンネル名'][0]
    # ToDo:チャンネル名だと変更された時に修正いれないといけないので、IDにしたい。
    #      がDiscordのチャンネルIDってどこからとるの??
    #################################
    # 借金額初期値登録:?loanset
    #################################
    if message.content.startswith("?loanset"):
        # 送り主がBotだった場合は反応しない
        if client.user != message.author:
            # メッセージを書きます
            loan_user_money  = message.content.replace("?loanset","")
            if int(loan_user_money) < 0:
                m = "```マイナスの値が入力されています。入力された金額を確認してください"+ str(loan_user_money) +"```"
            else:
                try:
                    # DBへ接続する
                    connector,cur = get_dbconnect()
                    # 借入金額を登録する
                    cur.execute(createSQL, (message.author.name ,int(loan_user_money)))
                    # コミットする
                    connector.commit()
                    # 表示用に金額を切り捨てする
                    loan_user_money = int(loan_user_money) // 10000
                    # 金額をメッセージで呟く
                    m = "```" + message.author.name + "さんの借入総額" + str(loan_user_money) + "万円を登録しました。```"
                except Exception as e:
                    m = "```" + message.author.name + "さんの借入総額は既に登録済みのようです。?infoコマンドで確認してください。```"                

            await client.send_message(message.channel, m)
    #################################
    # 借金額初期値登録:?reset
    #################################
    if message.content.startswith("?reset"):
        # 送り主がBotだった場合は反応しない
        if client.user != message.author:
            loan_user_money  = message.content.replace("?reset","")
            if int(loan_user_money) < 0:
                m = "```マイナスの値が入力されています。入力された金額を確認してください"+ str(loan_user_money) +"```"
            else:
                try:
                    connector,cur = get_dbconnect()
                    # 借入金額登録
                    cur.execute(updateSQL, (int(loan_user_money), message.author.name ))
                    connector.commit()
                    # 表示用に金額を切り捨てする
                    loan_user_money = int(loan_user_money) // 10000
                    m = "```" + message.author.name + "さんの借入総額" + str(loan_user_money) + "万円を再登録しました。```"
                except Exception as e:
                    m = "```" + message.author.name + "さんの借入総額が登録されていません。?loansetコマンドで登録してください。```"
            await client.send_message(send_channel, m)
            #await client.send_message(message.channel, m)
    #################################
    #  返済額入力登録:?repay
    #################################
    if message.content.startswith("?repay"):
        # 送り主がBotだった場合は反応しない
        if client.user != message.author:
            # メッセージを書きます
            loan_user_money  = message.content.replace("?repay","")
            if int(loan_user_money) < 0:
                m = "```マイナスの値が入力されています。入力された金額を確認してください"+ str(loan_user_money) +"```"
            else:
                try:
                    connector,cur = get_dbconnect()
                    # 現在金額取得
                    cur.execute(readPersonSQL, (message.author.name,))
                    loan = cur.fetchall()
                    # 総額から金額を引く
                    loan = loan[0][ 1] - int(loan_user_money)
                    # 返済総額の登録
                    cur.execute(updateSQL,(loan, message.author.name))
                    # 返済情報の登録
                    cur.execute(repaySQL,(message.author.name, loan))
                    connector.commit()
                    # 表示用に金額を切り捨てする
                    loan_user_money = int(loan_user_money) // 10000
                    # 表示用に金額を切り捨てする
                    loan = loan // 1000
                    m = "```" + str(loan_user_money) + "万円返済しました。" + message.author.name + "さんの借入残額は" + str(loan) + "万円です。```"
                except Exception as e:
                    print(e)
                    print(type(e))
                    m = "```返済額の登録に失敗しました。 @わいつぼいまで連絡をお願いします。```"
            await client.send_message(send_channel, m)
            #await client.send_message(message.channel, m)

    #################################
    # 借金新規借入登録:?add
    #################################
    if message.content.startswith("?add"):
        # 送り主がBotだった場合は反応しない
        if client.user != message.author:
            # 文字列から金額を取得する
            addloan = message.content.replace("?add","")
            if int(addloan) < 0:
                m = "```マイナスの値が入力されています。入力された金額を確認してください"+ str(addloan) +"```"
            else:
                try:
                    connector,cur = get_dbconnect()
                    # 現在金額の取得
                    cur.execute(readPersonSQL,(message.author.name,))
                    loan = cur.fetchall()
                    # 借金総額を加算
                    loan = loan[0][ 1] + int(addloan)
                    # 金額の更新
                    cur.execute(updateSQL,(loan, message.author.name))
                    connector.commit()
                    # 表示用に金額を切り捨てする
                    addloan = int(addloan) // 10000
                    # 表示用に金額を切り捨てする
                    loan = loan // 10000                 
                    m = "```" + message.author.name + "さんの借入額に" + str(addloan) + "万円追加しました。現在の借入総額は" + str(loan) + "万円です。```"
                except Exception as e:
                    print(e)
                    print(type(e))
                    m = "```" + message.author.name + "さんの情報登録に失敗しました。登録済みで本メッセージが表示される場合は@わいつぼいまで連絡をお願いします。```"
            await client.send_message(send_channel, m)
            #await client.send_message(message.channel, m)

    #################################
    # 総借入額表示:?total
    #################################
    if message.content.startswith("?total"):
        # 送り主がBotだった場合は反応しない
        if client.user != message.author:
            try:
                connector,cur = get_dbconnect()
                # 総借入金額取得
                cur.execute(readAllloanSQL, )
                Allloan = cur.fetchall()
                # 総登録ユーザ数を登録する
                cur.execute(readPersonCountSQL, )
                CountPerson = cur.fetchall()
                m = "```本BOTに登録された人数は" + str(CountPerson[0][0]) + "人で、借入額の総額は" + str(Allloan[0][0]) + "円です。```"
            except Exception as e:
                print(str(e))
                m = "```総借入額の取得に失敗しました。@わいつぼいまで連絡をお願いします。```"
            await client.send_message(message.channel, m)
    #################################
    # 個別登録額確認:?info
    #################################
    if message.content.startswith("?info"):
        # 送り主がBotだった場合は反応しない
        if client.user != message.author:
            try:
                connector,cur = get_dbconnect()
                # 個人借入データ取得
                cur.execute(readPersonSQL, (message.author.name,))
                personLoan = cur.fetchall()
                m = "```" + message.author.name + "さんのローン総額は" + str(personLoan[0][ 1]) + "円です。```"
            except Exception as e:
                print(e)
                print(type(e))
                m = "```" + message.author.name + "の登録情報取得に失敗しました。すでに登録済みで本メッセージが表示される場合は@わいつぼいまで連絡をお願いします。```"
            await client.send_message(message.channel, m)
    #################################
    # ヘルプコマンド:?help
    #################################            
    if message.content.startswith("?help"):
        if client.user != message.author:
            m = Usage
            await client.send_message(message.channel, m)

#################################
# DB接続処理
#################################
def get_dbconnect():
    for cnt in range( 1, 4 ):
        try:
            connector = MySQLdb.connect( host="******", db="*****", user="*****", passwd="*****", charset="utf8")
            cur = connector.cursor()
            break
        except Exception as e:
            print("DB接続に失敗しました。[" + str(cnt)  + "回目]" )
    else:
        print("DB接続に3回失敗しました。")
        raise

    return connector,cur

client.run(token)

基本的なところは、「python discord bot」でググった時の上位記事に書いてあるので省略しますが、

この辺抑えとくと色々できると思うので、説明します。

message.contentで入力値が取れます。
例えば初期登録コマンドで

?loanset 100000

と実行した場合、

message.contentから取得できる値は

print(message.content)
> ?loanset 100000

となります。

半角スペースでコマンドの後に指定した値を取得するために

message.content.replace(“?loanset”,””)

としています。
replaceは「○を○に置き換える」なので、今回の場合は

「?loanset」を「」に置き換えるので

後ろの100000だけを取り出すことができます。

print(message.content.replace("?loanset",""))
> 100000

こんな感じです。

あとは取り出した値をDBに格納すればOK。

この辺はPythonのお作法を参考してもらえばうまくいくと思います。

また、色んなところでBOTが実行結果を返さないように

発言するチャンネルは下記の通り指定しました。

    send_channel = [channel for channel in client.get_all_channels() if channel.id == 'チャンネルid'][0]

上記を指定して、発言時に上記で設定したチャンネルを指定すれば、

指定したチャンネルにBOTが出力したい内容を出力することができます。

await client.send_message(send_channel, m)

動作環境準備

手元で確認はとれました。が、

自分の開発機(MacBookPro)をずっと付けっ放しにしておくわけにはいかないので、

動作させるためのサーバを用意。

今回の動作環境ではGoogle Cloud Platformを使いました。

ubuntuでVMを立てて、Python・MySQLとその他・動作に必要なモジュールをインストール。

今回のプログラムをして試し起動。

試し起動の接続先として、プログラムテスト用にdiscordのサーバを1つ作成して

今回のプログラムテスト用のdiscordで起動。

・・・動いた!

追加要望実装

自分だけテストしても自己満足で終わってしまうので、

今回コマンドでの実装をコメントしていただいたりょーへいさんと

借金部部長のくりりんさんに

プログラムテスト用のdiscordサーバへ招待して、

実際に触ってもらうことに。

結果、

・表示を円表示ではなく万円表示に
 -> 1000で割って、あまりは切り捨てで実装
・1万以下も表示したい
 -> 単純に1000で割る、型をintではなくfloatに

という感じで追加実装しました。

本番試験

ブロガーズキルドで動かすためには

アカウントをサーバ側で作成・承認してもらい

Tokenを共有してもらう必要があります。

今回はGMのイケハヤさんにBOT用アカウントを作成してもらい、

ブロギル用のTokenを設定し、テストように?helpコマンドを実行。

借金部のチャンネル上でのみ、BOTの発言が出ることを確認し、今回の開発は終了。

今の所特に問題も出てないですし、よかったです。

まとめ

今回初めてDiscord BOTを書きましたが、

ネットに上がってる情報からそれなりに書けました。

仕事でしか活かせてないプログラミングの知識を

ブロガーズギルドで活かすことができて楽しいです。

強いて言えば、これで更にマネタイズできると嬉しいですね・・・

この借金BOTもマネタイズできるといいんですが・・・

プログラミング部として、開発受付中ですので、

是非是非プログラミング部まで〜

わいつぼい指名でも嬉しいですー。

おしまい。