--- title: Seina-18147 date: 2021-07-30 categories: - projects - 學習歷程們 tags: - projects - 學習歷程們 --- # 自訂義聊天機器人 ###### tags: `Project` [Github](https://github.com/URLoser404/Seina-18147) [Blog Doc](https://urloser404.github.io/projects/Seina-18147.html#%E5%89%8D%E8%A8%80) [hackmd](https://hackmd.io/@IWLYF/r1YL_MAXY) [Seina-18147](https://discord.com/oauth2/authorize?client_id=873627960685514802&permissions=8&scope=bot) ## 目錄 [TOC] ## 前言 從會寫程式開始的第一個side project 便是discord bot 還記得最開始的discord bot 便是最樸實的純聊天機器人 無限的往程式碼加入if 來加入回復 半年過去 回去看自己的程式碼只能說頭真的很痛 但看到這又想看看自己到底提升了多少 所以我決定來重現一下這個功能 並把它做得更好 更有趣 ## 理想 還記得看到之前的程式碼 添加的回覆全部都在主程式裡瘋狂添加if 所以這次希望至少可以使用json來儲存回復 並希望可以有明確的專案結構 不要再同一個檔案寫到底 導致程式碼可讀性差 ## 過程 ### 基本架構 #### 建立基本檔案 首先要決定整個專案的結構 因為discord bot需調用本身的api的關係 專案結構必須根據官方的cog方法來建置 ``` ├── functions(cog資料夾) │ ├── 功能分類一.py │ └── 功能分類二.py ├── 機器人主程式.py └── setting.json ``` 之後去discord developers申請一隻discord bot 點application ![](https://i.imgur.com/g2hDSWL.png) 之後新增一個application ![](https://i.imgur.com/YNY2TZB.png) 點bot ![](https://i.imgur.com/B7TIxTY.png) 在application中新增機器人使用者 ![](https://i.imgur.com/BMDLCbZ.png) 之後複製token放入設定檔 ![](https://i.imgur.com/TMfQhGE.png) ```json { "token" : "複製過來的token" } ``` 再來導入discord的函式庫 ```sh pip install discord ``` 在主程式中加入機器人基本設定 ```py import discord from discord.ext import commands import json import os with open('setting.json','r',encoding='utf8') as file: # 開啟設定檔 setting = json.load(file) # bot = commands.Bot(command_prefix='&') # 建立機器人物件 for file in os.listdir('./functions'): # 逐一讀取cog功能 if file.endswith('.py'): # bot.load_extension(f'functions.{file[:-3]}') # if __name__ == '__main__': bot.run(setting["token"]) # 讀入設定檔中機器人金鑰 ``` #### 建立其他功能(cog) 檔案 cog架構是以物件的方式分開功能 一功能分類用一類別撰寫 範例如下 ```py import discord from discord.ext import commands class 功能分類一(commands.Cog): def __init__(self,bot): self.bot = bot def setup(bot): bot.add_cog(功能分類一(bot)) ``` #### 功能加載 用cog架構的好處 就是能夠透過指令開關加載、卸載分類 從而達到不關掉機器人 且可以更新程式碼的效果 接下來將在主程式中加入加載、卸載功能 ```py @commands.command() @commands.is_owner() # 機器人擁有者才可使用 async def load(self,ctx,extention): # 加載功能 self.bot.load_extension(f'functions.{extention}') await ctx.message.delete() await ctx.send(f'Load function {extention} successfully') @commands.command() @commands.is_owner() async def unload(self,ctx,extention): # 卸載功能 self.bot.unload_extension(f'functions.{extention}') await ctx.message.delete() await ctx.send(f'Un - Load function {extention} successfully') @commands.command() @commands.is_owner() async def reload(self,ctx,extention): # 重新載入功能 self.bot.reload_extension(f'functions.{extention}') await ctx.message.delete() await ctx.send(f'Re - Load function {extention} successfully') ``` ### 自定義聊天主功能 完成基本架構後 便開始製作主要的自訂聊天功能 #### 決定json 首先要決定json的格式 因為考慮要在複數伺服器使用 所以json最外層需使用伺服器id 內層分別為四種觸發模式 - contain 包含 - equal 等於 - startwith 開頭為 - endwith 結尾為 範例如下 ```json { "伺服器id 1": { "contain": { "觸發字串一": [ "回復字串一", "回復字串二" ], "觸發字串二": [ "回復字串三", "回復字串四" ] }, "equal": { "觸發字串一": [ "回復字串一", "回復字串二" ], "觸發字串二": [ "回復字串三", "回復字串四" ] }, "startwith": { "觸發字串一": [ "回復字串一", "回復字串二" ], "觸發字串二": [ "回復字串三", "回復字串四" ] }, "endwith": { "觸發字串一": [ "回復字串一", "回復字串二" ], "觸發字串二": [ "回復字串三", "回復字串四" ] } }, "伺服器id 2": { "contain": { "觸發字串一": [ "回復字串一", "回復字串二" ], "觸發字串二": [ "回復字串三", "回復字串四" ] }, "equal": { "觸發字串一": [ "回復字串一", "回復字串二" ], "觸發字串二": [ "回復字串三", "回復字串四" ] }, "startwith": { "觸發字串一": [ "回復字串一", "回復字串二" ], "觸發字串二": [ "回復字串三", "回復字串四" ] }, "endwith": { "觸發字串一": [ "回復字串一", "回復字串二" ], "觸發字串二": [ "回復字串三", "回復字串四" ] } } } ``` #### 撰寫新增函式 ```py def update_reply(mode,context,reply,server_id): jsonFile = open("reply.json", "r",encoding='utf8') # 開啟json data = json.load(jsonFile) # 存取暫存 jsonFile.close() # 關閉json if server_id not in data.keys(): # 若server未曾存入 初始化 json data[server_id] = { # "contain":{}, # "equal":{}, # "startwith":{}, # "endwith":{} # } # if context not in data[server_id][mode].keys(): # 若觸發字串未曾存入 初始化 json data[server_id][mode][context] = [] # if reply not in data[server_id][mode][context]: # 若回復字串未曾新增 加入回復 data[server_id][mode][context].append(reply) # jsonFile = open("reply.json", "w+",encoding='utf8') # 開啟json json.dump(data,jsonFile, indent=4,ensure_ascii=False) # 更新資料 jsonFile.close() # 關閉json embed=discord.Embed(title="reply message", description="the reply you just add", color=0xa7f21c) # 生成discord embed訊息 embed.add_field(name=mode, value=f"`{context}`", inline=True) # embed.add_field(name="reply", value=f"`{reply}`", inline=True) # return embed # 回傳提示訊息至主程式 ``` #### 撰寫新增指令 因為功能相近 這裡使用了discord特殊的子命令結構 使用方法須先撰寫主命令名稱 ```py @commands.group() async def define_message(self,ctx): pass ``` 之後於主命令下新增四個主命令 直接對應到剛剛json的四個觸發模式 指令裡直接呼叫剛剛的新增函式 ```py @define_message.command( help="add reply message when message contain the context", brief="add reply message when message contain the context" ) async def contain(self,ctx,context,reply_message): embed = update_reply("contain",context,reply_message,ctx.message.guild.id) await ctx.send(embed=embed) @define_message.command( help="add reply message when message equal the context", brief="add reply message when message equal the context" ) async def equal(self,ctx,context,reply_message): embed = update_reply("equal",context,reply_message,ctx.message.guild.id) await ctx.send(embed=embed) @define_message.command( help="add reply message when message startwith the context", brief="add reply message when message startwith the context" ) async def startwith(self,ctx,context,reply_message): embed = update_reply("startwith",context,reply_message,ctx.message.guild.id) await ctx.send(embed=embed) @define_message.command( help="add reply message when message endwith the context", brief="add reply message when message endwith the context" ) async def endwith(self,ctx,context,reply_message): embed = update_reply("endwith",context,reply_message,ctx.message.guild.id) await ctx.send(embed=embed) ``` #### 撰寫偵測事件 最後一步就是在訊息發送事件中加入偵測 ```py @commands.Cog.listener() # cog內event特殊寫法 async def on_message(self, message): if not message.author.bot: # 避免偵測機器人訊息 if not message.content.startswith("http"): # 過濾網址 with open('reply.json','r',encoding='utf8') as file: # 開啟json reply = json.load(file) # 存入暫存 server_id = str(message.guild.id) # 暫存伺服器id if server_id in reply.keys(): # 偵測伺服器是否有加入回復訊息 msg = [] # 吻合回復字串庫 for i in reply[server_id]["contain"].keys(): # 搜尋並加入回復字串庫 if i.lower() in message.content.lower(): # msg += reply[server_id]["contain"][i] # for i in reply[server_id]["equal"].keys(): # 搜尋並加入回復字串庫 if i.lower() == message.content.lower(): # msg += reply[server_id]["equal"][i] # for i in reply[server_id]["startwith"]: # 搜尋並加入回復字串庫 if message.content.lower().startswith(i.lower()): # msg += reply[server_id]["startwith"][i] # for i in reply[server_id]["endwith"]: # 搜尋並加入回復字串庫 if message.content.lower().endswith(i.lower()): # msg += reply[server_id]["endwith"][i] # if(len(msg) > 0): await message.reply(choice(msg)) # 從字串庫中隨機挑選字串回復 ``` 此功能變大工告成 #### 效果 使用指令新增回復 ![](https://i.imgur.com/Gdx3yFF.png) 機器人回復剛剛新增的訊息 ![](https://i.imgur.com/N17HUiw.png) ### 其他功能 為了讓機器人變得更有趣 我還加入了很多奇怪、沒有用 但很有趣的功能 因為有點多 而且真的不太有用 所以將用程式碼加上註解的方式來呈現 #### 隨機顏色產生 ```py @commands.command() async def color(self,ctx): from random import randint # 導入隨機函示庫 from PIL import Image, ImageDraw # 導入圖片函示庫 r,g,b = [randint(0,255) for i in range(3)] # 隨機產生 rgb 三數值 color_str = "#" # 將 rgb 數值轉換成 16 進制字串 for i in [r,g,b]: # color_str += hex(i)[2:].upper() # img = Image.new("RGB", (300,300) ,(r,g,b)) # 用函示庫產生純色色塊 img.save("color.jpg") # embed = discord.Embed(title="Seina pick a color for you", description = color_str, color=discord.Color.from_rgb(r,g,b)) # 生成及傳送discord embed訊息 file = discord.File("color.jpg", filename="color.jpg") # embed.set_image(url=f"attachment://color.jpg") # await ctx.send(embed=embed , file = file) # ``` ![](https://i.imgur.com/lTDUut6.png) #### 天氣查詢功能 ```py @commands.command() async def weather(self,ctx,location): import requests # 導入api爬取所需的request庫 await ctx.message.delete() # 刪除訊息 url = 'https://opendata.cwb.gov.tw/fileapi/v1/opendataapi/F-D0047-091?Authorization=CWB-97AF1200-D64F-4BD7-BFBA-C8E9892538FC&downloadType=WEB&format=JSON' # 政府公開天氣資料api的url res = requests.get(url) # 爬取資料 location_to_code = { # 將文字轉換成資料裡城市的編號 '連江': 0, # '金門': 1, # '宜蘭': 2, # '新竹': 3, # '苗栗': 4, # '彰化': 5, # '南投': 6, # '雲林': 7, # '嘉義': 8, # '屏東': 9, # '臺東': 10, '台東': 10, # '花蓮': 11, # '澎湖': 12, # '基隆': 13, # '新竹': 14, # '嘉義': 15, # '臺北': 16, '台北': 16, # '高雄': 17, # '新北': 18, # '臺中': 19, '台中': 19, # '臺南': 20, '台南': 20, # '桃園': 21 # } data_we = res.json() description = data_we['cwbopendata']['dataset']['locations']['location'][location_to_code[location]]['weatherElement'][14]['time'][0]['elementValue']['value'] # 氣象描述 location = data_we['cwbopendata']['dataset']['locations']['location'][location_to_code[location]]['locationName'] await ctx.send(location+"天氣:\n"+"```\n"+description+"```\n") ``` ![](https://i.imgur.com/8H6fuhd.png) #### 泡泡紙功能 ```py @commands.command() async def pop(self,ctx,word='pop',width=10,height=10): if (len(word) + 4)*width*height > 1000: # 檢測泡泡紙大小是否超過discord傳送訊息限制 await ctx.send("oops, the pop is too big") # 送出錯提示訊息 else: await ctx.message.delete() # 刪除訊息 embed = discord.Embed(title="Seina give you a bubble paper", description = (('||'+word+'||')*width+'\n')*height, color = 0xffffff) # 送出泡泡紙 await ctx.send(embed = embed) #送出泡泡紙 ``` ![](https://i.imgur.com/okLqfyK.png) #### 踢出伺服器成員 ```py @commands.command() @commands.is_owner() # 機器人擁有者才可使用功能 @commands.has_permissions(kick_members=True) # 檢測機器人是否有踢人權限 async def kick(self, ctx, member : discord.Member, *, reason=None): await member.kick(reason=reason) # 踢出 await ctx.send(f'Banned {member.mention}') # 傳送提示訊息 ``` 這個就不測試了 傷感情 ### 選定伺服器架設 discord bot是以客戶端(client) 的方式在discord中活動的 所以只需要對主程式進行偵錯(debug) 便可以運行機器人 但一直運行在家中的電腦肯定不是長久之計 後來多方考慮下還是選擇架設在heroku #### 加入heroku必要設定檔 - Procfile 設定heroku伺服器運行模式之檔案 ``` worker python 主程式.py ``` - runtime.txt 設定伺服器運行主環境 ``` python-3.8.5 ``` - requirements.txt 伺服器必要下載套件 須將機器人所用到的函示庫加入其中 ``` discord requests Pillow discord-together ``` #### 創建heroku專案 接下來在heroku中加入新專案 ![](https://i.imgur.com/ZbubXzX.png) 之後使用終端機指令將專案推上heroku 需安裝git工具 及 heroku cli ```sh heroku login // 登入heroku cd 專案名稱 // 專換路徑至專案 git init // 初始化git heroku git:remote -a heroku專案名稱// 增加heroku推送路徑 git add . // 推送至heroku git commit -m "推送描述" // git push heroku master // ``` #### 推送完成 完成這些後 discord bot就會風雨無阻的工作了喔 ![](https://i.imgur.com/wCESu9r.png) ## 心得 discord bot 真的是從開始寫程式以來我就很喜歡的介面 每次有想到甚麼功能都會想要用這種表現形式來呈現 這次除了讓我更能夠明確的規劃專案 更多的是讓我熟悉了自己的開發過程跟節奏 也完成了看到自己以前程式碼之後的小測試 看到一年內就算沒有多少 自己也一定有進步 專案的結構、程式碼可讀性都比以前高上很多 不知道明年的自己是不是還會持續進步呢