replitでdiscordの身内用botを何度か作り直したときの副産物である、コードの修正や個人的に使いやすかったコピペをまとめておく。
replitでほぼ24時間動くdiscord botを作る方法はこちらを参照。ただし、discord.py
の更新により、そのままでは実行できない。
実際に自分が作ったコードはここから確認できる。ただし、身内ノリの関係でう〇こが大量に出てくるので注意。
- #赤で表示されている部分を消す
+ #緑で表示されている部分を追加する
修正
intentsの適用
- client=discord.Client()
+ intents=discord.Intents.default()
+ intents.message_content=True
+ client=discord.Client(intents=intents)
いつぞやのdiscord.py
の更新で、そのままだとTypeError : __init__() missing 1 required keyword_only argument : 'intents'
が出てくるが、client=discord.Client(intents=discord.Intents.default())
と直しても(少なくともreplit上で起動した場合は)discord上の発言を読み取れない。
トークンの隠蔽
- TOKEN = os.getenv("TOKEN")
- client.run(TOKEN)
+ client.run(os.getenv('TOKEN'))
bot内に管理者以外が使えるeval
やexec
がある場合、TOKENを定義してしまうとglobals()
を実行するだけで簡単にTOKENが盗まれる。
再起動スムーズ化
- keep_alive()
- client.run(os.getenv('TOKEN'))
+ try:
+ keep_alive()
+ client.run(os.getenv('TOKEN'))
+ except Exception as e:
+ print(str(e))
+ os.system("kill 1")
429 Too Many Requests (error code: 0)
などで長時間起動しないような事態がかなり減る。
オンライン表示
恐らく、keep_alive.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 keep_alive
+ from flask import Flask
+ from threading import Thread
+
+ app = Flask('')
+ @app.route('/')
+ def main():
+ #HTMLコードをreturnする
+ return "<html>Hello</html>"
+
+ def run():
+ app.run(host="0.0.0.0", port=8080)
+ def keep_alive():
+ server = Thread(target=run)
+ server.start()
実装例
ffadata.charadata
に格納されているセーブデータについて、レベルによる順位付けをオンラインで表示する。def main():
以外は同じ
def main():
rankdata = sorted([[id,ffadata.charadata[id]["lv"]] for id in list(ffadata.charadata.keys())], key=lambda x:x[1], reverse=True)
t="<br><midashi>FFAもどき レベルランキング</midashi><br><table><thead><tr><td></td><td></td><td></td></tr></thead><tbody>"
for i in range(len(rankdata)):
data=rankdata[i]
try:
t+="<tr><td>"+str(i+1)+"位</td><td>"+iroiro.idname[data[0]]+"さん</td><td>lv"+"{:,}".format(data[1])+"</td></tr>"
except:
t+="<tr><td>"+str(i+1)+"位</td><td>id"+str(data[0])+"さん</td><td>lv"+"{:,}".format(data[1])+"</td></tr>"
return """<!DOCTYPE html><html><head>
<title>Atmk Bot 24h Online Review</title>
<style type="text/css">
midashi{font.size=150%; font-weight: bold;}
</style></head><body>
<midashi>Atmk bot 24h オンライン表示</midashi><div id="DateTimeDisp"></div>
※1分経過で自動更新されます<br>
"""+t+"""<script>
setTimeout(function () {location.reload();}, 60000);var now = new Date();
var target = document.getElementById("DateTimeDisp");
var Year = now.getFullYear();
var Month = now.getMonth()+1;
var Date = now.getDate();
var Hour = now.getHours();
var Min = now.getMinutes();
var Sec = now.getSeconds();
target.innerHTML = Year + "/" + Month + "/" + Date + " " + Hour + ":" + Min + ":" + Sec;
</script></body></html>"""
便利コピペ
db使用セーブデータクラス
class Savedata:
def __init__(self,name):
self.savename=[name]
if self.savename[0] in db:
self.load()
def save(self):
db[self.savename[0]]="\n".join(["self."+attr+"="+str(eval("self."+attr)) for attr in dir(self) if not callable(getattr(self,attr)) and not attr.startswith("__")])
def load(self):
exec(db[self.savename[0]])
使用例
class Iroiro(Savedata):
def __init__(self,name="iroiro"):
self.counta=dict()
super().__init__(name)
iroiro=Iroiro()
@client.event
async def on_message(message):
if message.content=="あ":
if not message.author.id in iroiro.counta:
iroiro.counta[message.author.id]=0
iroiro.counta[message.author.id]+=1
iroiro.save()
with open(ファイル名) as f:
などの構文を利用すれば、replit未使用でも利用可。
このSavedata
クラスを使う場合、str型をデータに含める場合は必ず["hogehoge"]
など、str型でない形 にすること。(上記のsave
関数では、str型をstr型として保存できない。)
- #NG
- self.strvalue = "hogehoge"
- #save()後にload()すると以下のように解釈され、エラーとなる
- self.strvalue = hogehoge
+ #OK
+ self.strvalue = ["hogehoge"]
+ #この場合は正常に保存・読込が行われる。
txtファイルのみの発言の際、txtファイルの中身を本文と解釈する
@client.event
async def on_message(message):
CONT=message.content
if CONT=="":
try:
r=requests.get(message.attachments[0].url, stream=True, timeout=(3,7))
r.encoding="utf-8"
if r.status_code == 200:
CONT=r.text.replace("\r","")
except:
pass
これを入れた後、message.content
の代わりにCONT
を使えばよい。
管理者用デバッグ機能
@client.event
async def on_message(message):
if ID==管理者のID:
if CONT.startswith(">debug"):
if not "\n" in CONT:
try:
await sendmes(message,str(eval(CONT[7:])))
except Exception as e:
await sendmes(message,str(e))
elif CONT.endswith("```"):
try:
codes = "\n".join(CONT.split("\n")[2:-1])
exec("mes=''\ndef print(text):\n global mes\n mes+=str(text)+'\\n'\n"+codes)
global mes
if mes=="":
await sendmes(message,"DONE")
else:
await sendmes(message,mes)
except Exception as e:
await sendmes(message,str(e))
else:
try:
await sendmes(message,CONT[7:])
except:
pass
>debug 変数名
と入れれば変数名の中身が出力され、
>debug
```python
pythonコード
```
と入力すればpythonコードが実行される。また、
>debug
文章
と入力すれば文章をそのまま出力する。
2000文字超過のテキストをtxtファイルに変換 & 編集・削除でbotの発言を更新
class Zatuyou:
def __init__(self):
self.meslist=dict()
zatuyou=Zatuyou()
@client.event
async def sendmes(message,text,memo=False):
if memo==False and len(text)<2000:
botmes=await message.channel.send(text)
else:
with open("file/message.txt",mode="w",encoding='utf-8') as textfile:
textfile.write(text)
botmes=await message.channel.send(file=discord.File("file/message.txt"))
if message.author.id!=このbotのID:
if not message.id in zatuyou.meslist:
zatuyou.meslist[message.id]=set()
zatuyou.meslist[message.id].add(botmes)
if len(zatuyou.meslist)>=100: #ここの数字はメモリ上限が許す限り任意
zatuyou.meslist=dict()
@client.event
async def trydelete(message,text=""):
try:
await message.delete()
except:
if text=="":
pass
else:
await sendmes(message,text)
@client.event
async def on_message_edit(before, after):
if not after.author.bot:
if after.id in zatuyou.meslist:
for MES in zatuyou.meslist[after.id]:
await trydelete(MES)
del zatuyou.meslist[after.id]
await on_message(after)
@client.event
async def on_message_delete(message):
if not message.author.bot:
if message.id in zatuyou.meslist:
for MES in zatuyou.meslist[message.id]:
await trydelete(MES)
del zatuyou.meslist[message.id]
sendmes
の後半部分では、botの発言内容をメッセージid毎に保存する機能がある。保存したmessage
を参照して、可能ならば消す機能(trydelete
)をon_message_edit, on_message_delete
に入れてある。
ランキング
class Iroiro(Savedata):
def __init__(self,name="iroiro"):
self.idname=dict()
super().__init__(name)
iroiro = Iroiro()
@client.event
async def sendrank(message,data,title,tanni):
rankdata = sorted(data.items(), key=lambda x:x[1],reverse=True)
text=title+"\n"
j=1
for i in range(len(rankdata)):
data=rankdata[i]
try:
user=iroiro.idname[data[0]]
if data[0]==message.author.id:
text+="**"+str(j)+" 位 "+user+" さん : "+"{:,}".format(data[1])+" "+tanni+"**\n"
else:
text+=str(j)+" 位 "+user+" さん : "+"{:,}".format(data[1])+" "+tanni+"\n"
except:
text+=str(j)+" 位 id "+str(data[0])+" さん : "+"{:,}".format(data[1])+" "+tanni+"\n"
if i<len(rankdata)-1 and not data[1]==rankdata[i+1][1]:
j+=1
await sendmes(message,text)
@client.event
async def on_message(message):
ID=message.author.id
NAME=message.author.name
if not message.author.bot:
if not ID in iroiro.idname:
iroiro.idname[ID]=NAME
iroiro.save()
例えばawait sendrank(message,iroiro.counta,"「あ」発言回数ランキング", "回")
のように使う。
自作エラー
class BotError(Exception):
pass
import traceback
@client.event
async def on_message(message):
if not message.author.bot:
try:
if message.content=="「あ」大好き":
if iroiro.counta[message.author.id]<100:
raise BotError("もっと「あ」と発言してください")
await message.channel.send("そうですね、あなたは実際"+str(iroiro.counta[message.author.id])+"回「あ」と発言しました")
except:
await message.channel.send(message.author.name+"さん"+"`"*3+"\n"+traceback.format_exc()+"`"*3)
メッセージの反応幅を広げる
正規表現を使うとよい。ここを見れば全コマンドが分かる。ビジュアライザーもある。
import re
class Zatuyou:
def __init__(self):
self.multia=re.compile(u"^[aAあア]+$")
zatuyou = Zatuyou()
@client.event
async def on_message(message):
if zatuyou.taika.match(CONT):
if ID in iroiro.counta:
iroiro.counta[ID]+=1
else:
iroiro.counta[ID]=1
上記の場合、a,A,あ,ア
の4種類のみからなる文章全てに反応する。