まえがき
Discord始めて3日目の一般大学生が試行錯誤しながら無料で24時間稼働するDiscordBotを作成した日記です
不正確な情報が混ざってるかもしれませんがBotを作りたい誰かの助けになればなと思っています!
基礎編
この基礎編では、$Hello
と投稿したらBotからHello!
と投稿する単純なBotを作成します!
24時間運用までやります!
DISCORD DEVELOPER PORTALに登録
こちらのリンクにアクセスしてください
New Application
をクリックしてBotの写真とか名前を設定します
左のメニューのBotに移動
PUBLIC BOT
→off
(自分だけがBotをサーバーに追加できる設定)
MESSAGE CONTENT INTENT
→on
(Botがメッセージテキストを取得するための設定)
Reset Token
からTokenを取得できるのでコピーして保存しときましょう
(Resetすると古いTokenは無効となります)
ここまでで初期設定は完了です
サーバーに追加
OAuth2
→URL Generator
SCOPES:bot
BOT PERMISSIONS:必要な権限つける
(今回はSend Messages
とRead Message History
)
下に出てくるURLの場所にアクセスして任意のサーバーに追加する
認証が終了するとDiscord側からBotがサーバーに参加していることが確認できます
(私のBotの名前は働かない社畜にしてます)
ロールの作成
ここでBotにロールを与えると良いと思います
Botは作成した時点でそのBot名と同じ名前のロールが与えられますが、それ以外にBot用のロールを作成しておくことをお勧めします
今回は投稿を取得してテキストを投稿するだけなので作成したロールに権限は特に与えていません
pythonでBotを制御
Botを作るライブラリはいくつかありますが、今回はdiscord.pyを使います
# 仮想環境の作成、有効化
conda create -n discord
conda activate discord
# dotenvをインストール
conda install python-dotenv
# discord.pyのインストール(音声サポートなし)
# Linux/macOS
python3 -m pip install -U discord.py
# Windows
py -3 -m pip install -U discord.py
python3 -V
> Python 3.11.5
pythonのバージョンは3.8以降でないとのちに使うコードが動かなくなる可能性があります
新規フォルダを作成し、pythonファイルを作成しましょう!
私はdiscord
というフォルダの中にmain.py
ファイルを作成しました
import discord
import os
from dotenv import load_dotenv
load_dotenv()
class MyClient(discord.Client):
async def on_ready(self):
print(f'ログインしました: {self.user}')
async def on_message(self, message):
print(f'送信: {message.author}: {message.content}')
if message.author == self.user:
return
if message.content == '$Hello':
await message.channel.send('Hello!')
intents = discord.Intents.default()
intents.message_content = True
client = MyClient(intents=intents)
client.run(os.getenv('TOKEN'))
次に、BotのTokenを.envファイルに格納しましょう
フォルダに.env
ファイルを作成します
TOKEN=MTE1MTg2NjE4ODIzMDU3MDAxNA.GBqPpE.sNjU_Hufy4OIxjtb1FDEJ09GW8KkqJWgWQ
このコードは自分のTokenに書き換えてください
これでmain.py
からos.getenv('TOKEN')
でTokenを受け取ることができます
cd discord
python3 main.py
実行すると、ログインしました: <Botの名前>
と表示されるはずです
Discordに移動すると追加したBotがオンライン状態になっていることも確認できます
確認できたら任意のチャンネルで$Hello
と投稿してみましょう
このようにBotから返信が返ってくると成功です
24時間運用
ReplとGoogleAppsScriptを使って完全無料でBotを24時間運用していきます!
Replの使い方
replitに登録
私はgoogleアカウントで登録しました
Creat Repl
からReplを作成します
言語はPythonを指定し、Titleはテキトーで大丈夫です!
エディタが表示されたら先ほどのmain.pyに書かれているコードをそのままreplのmain.pyにコピペしましょう
環境変数の登録
ToolsのSecretsをクリックし、New SecretからTOKENを設定します
入力したらAdd Secret
登録できると、TOKENの取得方法が表示されるので少しmain.py
の中身を書き換えます
import discord
import os
- from dotenv import load_dotenv
- load_dotenv()
class MyClient(discord.Client):
async def on_ready(self):
print(f'ログインしました: {self.user}')
async def on_message(self, message):
print(f'送信: {message.author}: {message.content}')
if message.author == self.user:
return
if message.content == '$Hello':
await message.channel.send('Hello!')
intents = discord.Intents.default()
intents.message_content = True
client = MyClient(intents=intents)
- client.run(os.getenv('TOKEN'))
+ client.run(os.environ['TOKEN'])
Runを押すとResource足りませんみたいな警告が出て、課金を勧められましたが、バツを押して待機していると遅いですが動きました
ログインしました: <Botの名前>
常時稼働
flaskファイルを作成します
New fileからkeep.py
ファイルを作成します
from flask import Flask
from threading import Thread
app = Flask('')
@app.route('/')
def main():
return "Bot is alive"
def run():
app.run(host='0.0.0.0', port=8080)
def keep_alive():
server = Thread(target=run)
server.start()
main.py
を書き換えます
import discord
import os
+ from keep import keep_alive
class MyClient(discord.Client):
async def on_ready(self):
print(f'ログインしました: {self.user}')
async def on_message(self, message):
print(f'送信: {message.author}: {message.content}')
if message.author == self.user:
return
if message.content == '$Hello':
await message.channel.send('Hello!')
intents = discord.Intents.default()
intents.message_content = True
client = MyClient(intents=intents)
- client.run(os.environ['TOKEN'])
+ keep_alive()
+ try:
+ client.run(os.environ['TOKEN'])
+ except:
+ os.system("kill")
表示されたviewのURLをコピーして保存しておきましょう
ここまでできればreplを閉じてもBotが動いていることが確認できます
GASでサーバーを動かし続ける
ここからはGoogleAppsScriptで先ほど立ち上げたwebサーバーに一定時間毎にアクセスすることでBotを動かし続けます
replはwebサーバーに一定時間アクセスがないと止まってしまうからです
マイドライブに移動して新規→その他→GoogleAppsScriptでGASファイルを作成します
プロジェクトの設定からスクリプトプロパティを追加します
replURL
を先ほど立てたwebサーバーのURLに設定します
const keepRepl = () => {
const replURL = PropertiesService.getScriptProperties().getProperty('replURL')
const data = {}
const headers = { 'Content-Type': 'application/json; charset=UTF-8' }
const params = {
method: 'post',
payload: JSON.stringify(data),
headers: headers,
muteHttpExceptions: true
}
response = UrlFetchApp.fetch(replURL, params);
console.log(response)
}
このコードでは中身が空っぽのdata
を立てたwebサーバーに送るだけのGASです
keepReplを実行すると、特に意味はないですけどログが出てきます
注意
権限の確認をされたら、詳細から安全ではないページへ移動してください
次にトリガーの設定
分ベースのタイマーを5分おきにして保存すれば終了です
エラー格闘日記
1.message.contentが表示されない
Clientのインスタンス、
client = discord.Client()
これを作成しただけでは投稿されたメッセージの中身を取得することができませんでした
なのでintents
を定義しました
intents.message_content = True
これがメッセージの中身を取得するために必要になります
こっちでもいけました↓↓
- intents = discord.Intents.default()
- intents.message_content = True
- client = MyClient(intents=intents)
+ client = MyClient(intents=discord.Intents.all())
2.intentsの謎のエラー
Traceback (most recent call last):
File "main.py", line 14, in <module>
intents.message_content = True
AttributeError: 'Intents' object has no attribute 'message_content'
intentsが必要だから書いたのにまたエラーかあああ
このエラーはDiscord側の設定が問題です
Discord DeveloperのBotのMESSAGE CONTENT INTENT
をonにして、Tokenを再発行するとうまく動きました
pythonのバージョンが3.8より前だとここでエラー出ます
発展編
現在のBotでは$Hello
と投稿するとHello!
と返信するだけでつまらないので他の機能も実装してみました!
ネガポジランキング作成
感情分析AI
投稿されたテキストを感情分析AIを用いてネガティブかポジティブか判定していこうと思います
まずはローカルで挙動を確かめます!
pip install transformers
エラー地獄になった方へ
conda create nlp python=3.7
conda activate nlp
conda install pandas
conda install PyTorch
pip install transformers
pip install fugashi
pip install unidic-lite
このように一時的にpythonのversionを3.7に下げて、色々インストールすると私は動きました
from transformers import pipeline
text = '毎日がエブリデイ'
nlp = pipeline(
model="lxyuan/distilbert-base-multilingual-cased-sentiments-student",
return_all_scores=True)
print(nlp(text))
[[{'label': 'positive', 'score': 0.536488950252533},
{'label': 'neutral', 'score': 0.2318655103445053},
{'label': 'negative', 'score': 0.2316455841064453}]]
このようにnlp
にtext
を渡すとpositive度合い、neutral度合い、negative度合いを取得することができました!
DB作成
判定したネガポジを保存するためのDatabaseをsqliteで作成します
Table
カラム名 | 役割 |
---|---|
id | id |
guildid | サーバーid |
channelid | 投稿されたチャンネルのid |
userid | 投稿したuserのid |
body | 投稿内容 |
positive | positive度合い |
neutral | neutral度合い |
negative | negative度合い |
created_at | 投稿日 |
Tableの作成
import sqlite3
with sqlite3.connect('discord_db') as connection:
cursor = connection.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS Post (
id INTEGER PRIMARY KEY,
guildid INTEGER,
channelid INTEGER,
userid INTEGER,
body STRING,
positive REAL,
neutral REAL,
negative REAL,
created_at TIMESTAMP
)
""")
実行すると、discord_db
というファイルができていると思います
Tableにレコードをinsert
import sqlite3
with sqlite3.connect('discord_db') as connection:
cursor = connection.cursor()
insert_query = """
INSERT INTO Post (guildid, channelid, userid)
VALUES (?, ?, ?, ?, ?, ?, ?)
"""
cursor.execute(insert_query, [1, 2, 3])
これで、
guildidが1、channelidが2、useridが3、のように新しいレコードを追加できます
model.py
にまとめる
ここまでのネガポジ判定とDB作成、レコード追加までをReplのmodel.py
にまとめます!
from transformers import pipeline
import sqlite3
def record_text_sentiment(guildid, channelid, userid, body, created_at):
nlp = pipeline(
model="lxyuan/distilbert-base-multilingual-cased-sentiments-student",
return_all_scores=True
)
negaposi_list = [nlp(body)[0][i]['score'] for i in range(3)]
try:
with sqlite3.connect('discord_db') as connection:
cursor = connection.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS Post (
id INTEGER PRIMARY KEY,
guildid INTEGER,
channelid INTEGER,
userid INTEGER,
body STRING,
positive REAL,
neutral REAL,
negative REAL,
created_at TIMESTAMP
)
""")
insert_query = """
INSERT INTO Post (guildid, channelid, userid, body, created_at, positive, neutral, negative)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
"""
cursor.execute(insert_query, [guildid, channelid, userid, body, created_at] + negaposi_list)
except sqlite3.Error as e:
print('DBのエラー: ', e)
return negaposi_list
text = '毎日がエブリデイ'
print(record_text_sentiment(5,5,5,text))
negaposi_list
には要素数3のリストが入っています
positive, neutral, negativeです
例 [0.536488950252533, 0.2318655103445053, 0.2316455841064453]
Discordに投稿された情報を受け渡す
Discordに投稿された情報をmain.py
で受け取ってmodel.py
のrecord_text_sentiment関数に渡して、DBに記録させていきます!
import discord
import os
from keep import keep_alive
+ from model import record_text_sentiment
class MyClient(discord.Client):
async def on_ready(self):
print(f'ログインしました: {self.user}')
async def on_message(self, message):
if message.author == self.user:
return
+ negaposi = record_text_sentiment(message.guild.id, message.channel.id, message.author.id, message.content, message.created_at)
+ print(f'guildid: {message.guild.id}')
+ print(f'channelid: {message.channel.id}')
+ print(f'userid: {message.author.id}')
+ print(f'body: {message.content}')
+ print(f'created_at: {message.created_at}')
+ print(f'negaposi: {negaposi}')
intents = discord.Intents.default()
intents.message_content = True
client = MyClient(intents=intents)
keep_alive()
try:
client.run(os.environ['TOKEN'])
except:
os.system("kill")
ReplでRunすると、すごい赤文字で出てきましたww
ReplのShellから以下のコマンドを入力する
pip uninstall torch
pip uninstall transformers
pip install torch
pip install transformers
終わったらもう一度Runするとうまく動きました
ランキング作成
作成したDBからネガポジ等取得してランキングを作成します!
やること
- 毎週日曜日に今週のポジティブランキングとネガティブランキングをチャンネルに投稿
- 毎月月末に同様のランキング作成して投稿
毎週・毎月リマインド
- サーバーidと日付が与えられたら今日の日付から与えられた日数のサーバー内で投稿された投稿をデータベースから取得する
- userごとにpositiveとnegativeの平均をとる
- ランキングを作成する
- 日曜日か判定して任意のチャンネルに送信する
新しくget_ranking.py
を作成
import sqlite3
from datetime import datetime, timedelta
def getRecentData(guildid, pastDays):
start_date = datetime.utcnow() - timedelta(days=pastDays)
with sqlite3.connect('discord_db') as connection:
cursor = connection.cursor()
query = "SELECT * FROM Post WHERE guildid = ? AND created_at >= ?"
cursor.execute(query, (guildid, start_date))
posts = cursor.fetchall()
return posts
このgetRecentData関数でpastDays
日間のデータを取得できます
次に取得したレコードをuserごとにネガポジの平均を取っていきます
import sqlite3
from datetime import datetime, timedelta
+ import numpy as np
def getRecentData(guildid, pastDays):
start_date = datetime.utcnow() - timedelta(days=pastDays)
with sqlite3.connect('discord_db') as connection:
cursor = connection.cursor()
query = "SELECT * FROM Post WHERE guildid = ? AND created_at >= ?"
cursor.execute(query, (guildid, start_date))
posts = cursor.fetchall()
return posts
# 追記
def calculateUserSentiment(posts):
user_sentiment = {}
for post in posts:
userid = post[3]
sentiment = np.array(post[5:8])
if userid in user_sentiment:
user_sentiment[userid].append(sentiment)
else:
user_sentiment[userid] = [sentiment]
for userid, sentiments in user_sentiment.items():
user_sentiment[userid] = sum(sentiments) / len(sentiments)
positive_ranking = sorted(user_sentiment.items(), key=lambda x: x[1][0], reverse=True)
negative_ranking = sorted(user_sentiment.items(), key=lambda x: x[1][2], reverse=True)
return positive_ranking[:3], negative_ranking[:3]
これで、positive、negativeごとにランキングが出来上がりました!
次に、main.pyから毎週日曜日と毎月1日にランキングを作成して送信していきます
import discord
import os
import asyncio
from datetime import datetime, timedelta
from keep import keep_alive
from model import record_text_sentiment
from get_ranking import getRecentData, calculateUserSentiment
class MyClient(discord.Client):
async def send_ranking(self):
await self.wait_until_ready()
while not self.is_closed():
now = datetime.now() + timedelta(hours=9)
if now.weekday() == 6 and now.hour == 12:
channel = self.get_channel(int(os.environ['CHANNELID']))
posts = getRecentData(int(os.environ['GUILDID']), 7)
positive_ranking, negative_ranking = calculateUserSentiment(posts)
text = '今週のモチベランキング\npositive\n'
text += '\n'.join([
f'> {i}. {self.get_user(p[0])}'
for i, p in enumerate(positive_ranking)
])
text += '\nnegative\n'
text += '\n'.join([
f'> {i}. {self.get_user(n[0])}'
for i, n in enumerate(negative_ranking)
])
await channel.send(text)
if now.day == 1 and now.hour == 12:
channel = self.get_channel(int(os.getenv('CHANNELID')))
posts = getRecentData(int(os.getenv('GUILDID')), 30)
positive_ranking, negative_ranking = calculateUserSentiment(posts)
text = '今月のモチベランキング\npositive\n'
text += '\n'.join([
f'> {i}. {self.get_user(p[0])}'
for i, p in enumerate(positive_ranking)
])
text += '\nnegative\n'
text += '\n'.join([
f'> {i}. {self.get_user(n[0])}'
for i, n in enumerate(negative_ranking)
])
await channel.send(text)
await asyncio.sleep(60*60)
async def on_ready(self):
print(f'ログインしました: {self.user}')
self.loop.create_task(self.send_ranking())
async def on_message(self, message):
if message.author == self.user:
return
negaposi = record_text_sentiment(message.guild.id, message.channel.id,
message.author.id, message.content,
message.created_at)
print(f'guildid: {message.guild.id}')
print(f'channelid: {message.channel.id}')
print(f'userid: {message.author.id}')
print(f'body: {message.content}')
print(f'created_at: {message.created_at}')
print(f'negaposi: {negaposi}')
intents = discord.Intents.default()
intents.message_content = True
client = MyClient(intents=intents)
keep_alive()
try:
client.run(os.environ['TOKEN'])
except:
os.system("kill")
毎週日曜日の12時と毎月1日の12時にそれぞれランキングを送信するように設定しています
最後!
Seacretsを追加で登録して、Runしてみましょう
設定した時間になったら流れてくるはずです!
Noneになってますねww
これは、権限かと思ったのですが、権限ではありませんでした
get_user()
はキャッシュから情報を取得するためほとんど意味がなかったです。
代わりに、fetch_user()
を使うとうまくいきました!
await self.fetch_user(userId)
最後に一言
ここまでめちゃめちゃ長かったですが最後までご覧いただきありがとうございます!
発展編からは完全に趣味にのめり込んでしまった感が否めないですが、AI、sql周りのことも学べたかなと思います!
今後の展望としては、スラッシュコマンドを使ってランキングを作れるようにしたり、おそらくデータベースが重くなると全ての処理が重くなってしまうので処理の高速化についてアップグレードしたりしたいなと思っております
コードが全体的に汚いですが、頑張ってわかりやすいように書いたのでお許しください
それでは〜バイバイきん〜〜