---
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 真的是從開始寫程式以來我就很喜歡的介面
每次有想到甚麼功能都會想要用這種表現形式來呈現
這次除了讓我更能夠明確的規劃專案
更多的是讓我熟悉了自己的開發過程跟節奏
也完成了看到自己以前程式碼之後的小測試
看到一年內就算沒有多少 自己也一定有進步
專案的結構、程式碼可讀性都比以前高上很多
不知道明年的自己是不是還會持續進步呢