# 資訊期末專題 ###### tags: `期末` `Python` `discord Bot` ## 前言 [專題Github連結](https://github.com/Nues0913/Final_project.git) :::success 本篇專注在Discord.py架構上的解說與理解,統整我在Bot撰寫上所學到的知識與應用,並且以一個初心者的角度分享在專題開發上所踩到的雷與在除錯和爬找資料所得到的經驗。 ::: <!-- 這次專題選擇了Discord Bot作為我們的主軸,結合網路爬蟲撰寫一個用於查找資訊的機器人,由我負責在前端上代碼的撰寫與維護。專題的性質與平時所做的課堂作業有著巨大的差別,對於專題而言,程式碼需要明確、有條理的規劃與撰寫。在本次的期末專題中,我們明確的規劃了Bot的程式架構,嘗試許多模組與指令管理、版本控管。與以往解題取向的撰寫不同,在專題開發中有許多未知錯誤,需要自己去尋找資源、摸索代碼、一遍又一遍的Trial and Error反覆嘗試數天,只求撰寫出能夠運行的代碼。 不同於單純抓取模板改參數的機械式撰寫,我們深入去了解每一行代碼的運行原理和邏輯,研究每一種函式的用法,在為期兩個禮拜的開發中,有著許多不明與未知的錯誤與挑戰,但透過網路不斷網羅資源,學習多個模組的用法、每一個步驟背後的原理,對於 `asyncio` `class` `json` `decorator` 以及一個專題的開發有著進一步的理解,從一片懵懂無知,到現在能夠利用資源,快速對問題進行分析與除錯。本次專題帶給我的不僅僅是一個Disocrd Bot,對於規劃程式架構、指令管理、資料查找、團隊分工、程式設計,都帶給了我巨大的成長。 --> ## Discord Bot 架設 ### 機器人創建 首先到 https://discord.com/developers/applications 創建一隻機器人 點選右上角 `New Application` 輸入名稱,點選同意 ![](https://i.imgur.com/QRVWhbk.png) 網頁會跳轉到Appication頁面,點選Bot填好格子 ![](https://i.imgur.com/ssaF6r9.png) 下方有三個選項 分別是`PRESENCE INTENT` `SERVER MEMBERS INTENT` `MESSAGE CONTENT INTENT` 三者都是機器人與伺服器互動所必需的權限,請將其開啟並且點選 `Save Changes` ![](https://i.imgur.com/MNLcE45.png) 點選左方的 `OAuth2` --> `URL Generator` 在 `SCOPES` 裡選取 `bot`,`BOT PERMISSIONS` 選擇 `Administrator` ![](https://i.imgur.com/TT5bMJB.png) 這時候拉到下方,有一串 `GENERATED URL` ,將他貼到瀏覽器,就可以邀請機器人進服了 ![](https://i.imgur.com/IIX1Rfg.png) ![](https://i.imgur.com/yI0sRtt.png) 我們在撰寫程式碼前需要取得token,用於識別機器人 請妥善保存好token,防止洩漏遭到有心人士利用 ![](https://i.imgur.com/iFkDwBm.png) ## Discord Bot 撰寫 ### 撰寫與理解程式前可能需要的先備能力 身為事事講求效率的現代人,我們大可直接找模板抄下來把參數修一修, 但是這樣實在太乏味了,為了理解和實踐Discord Bot的運作過程, 我們可能需要先理解: - **物件導向的基本觀念** - **裝飾器(Decorator)的本質和用法** - **"協程"是何方神聖,async,await的用法與使用時機** - **Json檔案的讀取** 有了上述的先備知識能夠大大提升撰寫與理解的速度。 接下來會展示本次專題所撰寫的代碼,並且在要點處旁逐行解釋, 特別艱澀的部分將會以註解形式挑出來細說。 ### 開始撰寫機器人 首先尋找一個編程環境, 我們選擇的是 replit.com 作為我們的編程環境, 透過另外的監控網頁能夠實現機器人24小時運行。 對於專題撰寫,一般會將不同性質或功能的程式碼放置於不同的資料夾, 進入replit後開始創建資料夾、檔案,規劃大致的程式架構,方便我們除錯和管理, 以下是我們本次專題使用的架構: ### 程式架構 - **Files** <font color="#8E8E8E">#主資料夾</font> - <font>main.py</font> <font color="#8E8E8E">#主程式區</font> - **cogs** <font color="#8E8E8E">#功能資料夾</font> - app_with_button.py - <font>common.py</font> - <font>slashcommand.py</font> - **core** <font color="#8E8E8E"> #特殊用途資料夾</font> - <font>classes.py</font> <font color="#8E8E8E"> #class初始化</font> - **custommod** <font color="#8E8E8E">#自定義模組資料夾</font> - <font>tnfsh.py</font> - <font>keep_alive.py</font> <font color="#8E8E8E">#保持上線模組</font> - <font>readme.txt</font> - <font>token.json</font> ![](https://i.imgur.com/xiUecSQ.png) :::warning <font size = 5>**小建議**</font> 網路上能找到許多非官方的Disocrd擴充庫, 使用此類非原生模組在程式出錯時可能會面臨資源匱乏、求助無門等情況。 在此的程式碼全數使用Disocrd的原生庫 <a href="https://discordpy.readthedocs.io/en/stable/" target="_blank">Discord.py</a> 能夠在其<a href="https://discordpy.readthedocs.io/en/stable/" arget="_blank">Discord API</a>查詢相關用法,網上也有較多相關論壇,避免成為資源孤兒 ::: ## 主程式<font>main.py</font> 主程式區是我們主要管理與運行機器人的地方,所有額外撰寫的功能都會被讀取到這裡, 我們的大致編寫流程如下: 1. 引進所需的庫 2. 實體化機器人物件 3. 監聽伺服器(Discord端)狀態並初步設定bot 4. 讀入撰寫的Cogs同步到bot上 5. 寫了一個reload功能,方便我們在測試功能時重載 6. 在replit上讓程式24小時運行,請參考這則 [教學影片](https://youtu.be/UT1h9un4Cpo) 7. 讀取bot的token,啟用bot **<font>main.py</font>** ```python= import keep_alive import discord from discord.ext import commands import asyncio, json, os # 實體化Bot,command_prefix是設定指令的前綴詞,intents是我們要求的權限,這裡抓取所有權限 bot = commands.Bot(command_prefix='!', intents=discord.Intents.all()) # 註解1 裝飾器與on_ready() @bot.event # 註解2 有關協程async與await async def on_ready(): game = discord.Game('機器人活動') synced = await bot.tree.sync() # 將斜線指令(appcommands)同步到Discord上並回傳一個list await bot.change_presence(status=discord.Status.online, activity=game) print('目前登入身份:', bot.user) print(f'application command has synced {len(synced)} commands') # 已同步指令數量 # 註解3 cogs async def cogs(): # os.listdir()用於回傳指定資料夾中包含的檔案名稱或資料夾名稱列表(type=list) for filename in os.listdir("./cogs"): if filename.endswith("py"): # 走訪結尾為.py的檔案 await bot.load_extension(f"cogs.{filename[:-3]}") # #asyncio.run()是呼叫協程的一個方法,在這裡執行cogs()載入我們的cogs asyncio.run(cogs()) # 定義reload用於重新加載Cogs,可在撰寫機器人時實現熱重載 @bot.tree.command(name='reload', description='reload all commands') async def reload(interaction: discord.Interaction): for filename in os.listdir("./cogs"): if filename.endswith("py"): await bot.reload_extension(f"cogs.{filename[:-3]}") await interaction.response.send_message('重新載入指令完畢') print('user used reload') # 使用了讓replit不斷線的模組函式 keep_alive.keep_alive() # 使用with open()開啟主資料夾裡的token.json檔,並且取得Bot token with open('token.json', 'r', encoding='UTF-8') as j: Token = json.load(j) bot.run(Token["token"]) ``` ### token token保存在與main.py同階級的資料夾裡,在本專題中以json格式存檔 ```json= { "token" : "your_token_here" } ``` ### 註解: 1. ### **@bot.event 與 on_ready()** 1. **裝飾器**:@bot.event是一個裝飾器,他將下面的函式 `on_ready()` 作為參數,<br>對此函式進行修改和裝飾,使函式帶有裝飾器定義的行為。 2. **事件**:此裝飾器將 `on_ready()` 裝飾成了一個事件,事件指當某些事情發生時所觸發的動作,例如:新成員加入了伺服器、傳送訊息、新增反應等。 `on_ready` 是Discord.py提供的一個事件函數,機器人啟動後,客戶端已連線到Discord伺服器,且準備好接收和處理事件時,該函數會被觸發,之後就可以在這個事件中對bot進行各種初始化操作。<br>事件函式的用法可以在<a href="https://discordpy.readthedocs.io/en/stable/" arget="_blank">官方文件</a>中查詢。 4. **監聽**:在DiscordBot中,我們需要監聽這些事件,在事件發生時進行所需的處理。在此main.py裡,我們在事件觸發後定義了game物件,使用 `bot.tree.sync()` 將後續的指令同步到Discord上,使用` bot.change_presence()` 設定了機器人顯示的狀態,並且在控制台 `print` 出了一些事件以供我們監控。 2. ### **有關協程 async 與 await** 1. **協程(coroutine)**:簡單來說可以看成一個函式,能在中途中斷、中途返回值給其他協程、中途恢復、中途傳入參數。協程的最大優勢在於當一個協程阻塞的時候,使用者能透過調度,將執行權讓給其他協程,有效的將資源運用最大化。<br>在Python 3.5以上能夠透過 `async` `await` 實現協程的創建。<br><br>==為什麼撰寫DiscordBot會使用到協程?==:Python在對每一行程式運行的時候,都等到程式回應之後再進行下一行程式,屬於 IO 阻塞式的語言。在Discord中有一用戶呼叫機器人時,Python便會堵塞在處理該用戶的請求,而無法同時處理多個用戶,此時便需要協程進行調度,將效能妥善的分配運用。 2. **async**:使用`async def`定義一個協程,在第13行中定義了一個事件函式,並回傳給裝飾器進行處理。 3. **await**:當 Python 遇到 `await` 時,它會停止協程的執行並處理其他事情,直到它標記的函式完成它的工作,才會繼續接下來的協程。這允許您的程序同時執行多項操作,而無需使用線程或複雜的多進程處理。`await` 只能在 `async def` 定義的協程中使用,並且標記的必須是`awaitable` 4. **協程的調度**:在Python中,協程需要進行調度和執行才能運作。 `task`是協程被調度和執行的載體,這些task由事件迴圈(event loop)進行管理,以實現程式的非同步執行。而在使用discord模組時,協程的調度與管理已經被寫在了裝飾器中,一般而言,我們只需要建立協程並回傳給裝飾器,不用再去額外管理協程的調度。 3. ### **Cogs 機器人功能管理** Cogs是discord.ext其中的一個功能,會在下面以章節方式詳細介紹 ## Cogs ### 什麼是Cogs 根據 [Pycord Guide/Cogs](https://guide.pycord.dev/popular-topics/cogs) >Cogs, often known as modules or extensions, are used to organize commands into groups. This is useful for grouping commands that have the same general idea (such as moderation commands). This also helps to avoid making your bot's files messy and cluttered. 我們能夠得知Cog是在discord.ext模組裡的一個功能,用於更方便的管理功能與指令。在單個Cog子類報錯時不會影響到其他bot的功能,並且可以透過 `reload_extension` 及時重載程式碼。 ### 創建Cogs 首先創建一個Cogs子資料夾 Cogs資料夾一般用於存放指令類或功能類的程式碼,方便我們分類管理 ![](https://i.imgur.com/IbK4o6z.png) 在Cogs資料夾中新建一個py檔 在py檔中添加以下代碼 **a_example.py** ```python= import discord from discord.ext import commands #使用Cogs需要引入的模組 class a_example(commands.Cog): #繼承commands.Cog類,透過重寫父類的方法與屬性來自定義Cog def __init__(self, bot): #初始化類並定義self.bot self.bot = bot '''你寫的指令''' async def setup(bot): #定義一個協程(在某版本後需要使用協程做setup,以前是直接def) await bot.add_cog(a_example(bot)) #將a_example這個Cog給bot做讀取 ``` 接著要讓主程式讀取我們的Cogs 在主程式中添加以下代碼 **<font>main.py</font>** ```python= import os async def cogs(): for filename in os.listdir("./cogs"): #用os.listdir走訪所有cogs資料夾的文件(返回一個list) if filename.endswith("py"): #如果文件結尾是一個py檔 await bot.load_extension(f"cogs.{filename[:-3]}") #載入cogs asyncio.run(cogs()) #執行cogs() ``` 於是Cogs功能就建立好了 ### 繼承 如果不想在每次創建文件時重寫一次初始化 ```python= class a_example(commands.Cog): def __init__(self, bot): self.bot = bot ``` 可以另外創建一個父類,在需要時直接繼承進來 先創建一個<font>classes.py</font> **<font>classes.py</font>** ```python= from discord.ext import commands class Cog_Extension(commands.Cog): def __init__(self, bot): self.bot = bot ``` 後續的Cog程式碼可以直接import core.classes直接繼承下來 core是我們放classes.py的資料夾 **other_cog.py** ```python= from discord.ext import commands from core.classes import Cog_Extension # 直接引入父類 import asyncio class slashcommand(Cog_Extension): # 繼承Cog的功能 '''你寫的指令''' ``` ### load 、 reload 與 unload 剛剛有提到使用load加載寫好的Cogs 如果要寫卸載與重載的功能 可以寫成指令的方式(指令的寫法等等會提到) **卸載與重載** ```python= # reload @bot.tree.command(name='reload', description='reload') async def reload(interaction: discord.Interaction): for filename in os.listdir("./cogs"): if filename.endswith("py"): await bot.reload_extension(f"cogs.{filename[:-3]}") await interaction.response.send_message('重新載入指令完畢') print('user used reload') # unload @bot.tree.command(name='unload', description='unload') async def unload(interaction: discord.Interaction): for filename in os.listdir("./cogs"): if filename.endswith("py"): await bot.unload_extension(f"cogs.{filename[:-3]}") await interaction.response.send_message('卸載指令完畢') print('user used unload') ``` ## Discord module 常用功能介紹 以下介紹一些在撰寫DiscordBot中常用的功能語法, 沒有特別說明的**一律以在Cog的class中為主**。 ### 前綴指令(prefix_command) ![](https://i.imgur.com/zBjJcf5.png =400x179) 前綴指令是指在客戶端使用設定的字符呼叫的指令, 例如:`!help` `!dice` `#reload` `*test`, 在偵測前綴指令前要先對聊天室的訊息做監控, 在Cog與主程式中的監聽器的裝飾器寫法不一樣, 並且在一個類方法中記得要傳入self。 **裝飾器比較:** |<font>main.py</font>| Cog | | :----------------: | :-----------------: | | @bot.command() | @commands.command() | **Cog中的前綴指令寫法** ```python= class ...(): @commands.command() async def ping(self, ctx): await ctx.send('pong!') ``` **主程式中的前綴指令寫法** ```python= @bot.command() async def ping(ctx): await ctx.send("pong!") ``` ### 斜線指令(slash_command) ![](https://i.imgur.com/dupcFVw.png =400x269) 斜線指令泛指在頻道上使用`/`呼叫的指令, 斜線指令擁有完整的指令清單以及詳細的指令描述, 並且具有參數框提供給使用者做參數或選項的傳入。 **Cog中的類方法記得都要傳入self。** **裝飾器比較:** |<font>main.py</font>| Cog | | :----------------: | :-----------------: | | @bot.tree.command() | @app_commands.command() | 使用斜線指令前需要使用`sync()`將指令同步到客戶端上, 要同步的對象是負責管理我們指令的`CommandTree`, 通常寫在主程式中的 `on_ready()` 在機器人啟動時同步。 ```python= @bot.event async def on_ready(): await bot.tree.sync() # 注意這是一個awaitable物件 ... ``` **Cog中的斜線指令寫法** ```python= class ...(): @app_commands.command(name='name', description='description') async def test(self, interaction: discord.Interaction): # interaction是與客戶端互動後傳回的物件 await interaction.response.send_message('text') # 在同個interaction中只能做一次response await interaction.followup.send('text') # followup會提及上一則response await interaction.channel.send('text') # 這是在你response後還想再送出訊息的一個寫法 ``` **主程式中的斜線指令寫法** ```python= @bot.tree.command(name='name', description='description') async def test(interaction: discord.Interaction): # interaction是與客戶端互動後傳回的物件 await interaction.response.send_message('text') # 在同個interaction中只能做一次response await interaction.followup.send('text') # followup會提及上一則response await interaction.channel.send('text') # 這是在你response後還想再送出訊息的一個寫法 ``` ### 嵌入式訊息(Embed) ![](https://i.imgur.com/sZk75lS.png =400x270) 嵌入式訊息是discord其中一種訊息格式,支援markdown語法, 提供一個可排版與美化的介面。 以下是一個簡易的嵌入式訊息設定: ```python= an_embed=discord.Embed(title="title", description="description", color=0x00ff11) an_embed.add_field(name="Field 1", value="content", inline=False) an_embed.add_field(name="Field 2", value="content", inline=False) ``` 訊息預覽: ![](https://i.imgur.com/3BCyH6x.png =116x142) 更多版面設定請參略這邊的 [Pycord Guide/Embeds](https://guide.pycord.dev/getting-started/more-features#embeds) 根據官方說明: > await send(_content=None_, _*_, _tts=False_, <font color = D61D11>_embed=None_</font>, _embeds=None_, _file=None_, _files=None_, _stickers=None_, _delete_after=None_, _nonce=None_, _allowed_mentions=None_, _reference=None_, _mention_author=None_, _view=None_, _suppress_embeds=False_, _silent=False_)[¶](https://discordpy.readthedocs.io/en/stable/api.html?highlight=send#discord.TextChannel.send "Permalink to this definition") > await send_message(_content=None_, _*_, <font color = D61D11>_embed=..._</font>, _embeds=..._, _file=..._, _files=..._, _view=..._, _tts=False_, _ephemeral=False_, _allowed_mentions=..._, _suppress_embeds=False_, _silent=False_, _delete_after=None_)[¶](https://discordpy.readthedocs.io/en/stable/interactions/api.html?highlight=send_message#discord.InteractionResponse.send_message "Permalink to this definition") 送出Embed訊息只需要在`send()` `send_message()` `edit_message()`等函式中傳入 `embed=an_embed`, 後方的an_embed是我們定義的embed。 **一個Cog斜線指令中送出embed的範例** ```python= class ...(): @app_commands.command(name='name', description='description') async def func(self, interaction: discord.Interaction): # 設定一段embed an_embed=discord.Embed(title="title", description="description", color=0x00ff11) an_embed.add_field(name="Field 1", value="content", inline=False) an_embed.add_field(name="Field 2", value="content", inline=False) # 傳出embed await interaction.response.send_message('text',embed=an_embed) ``` ### 按鈕(Button) ![](https://i.imgur.com/HVVTArZ.png) 按鈕是disocrd裡一個和用戶互動的物件, 撰寫按鈕要先繼承 `discord.ui.View` 這個類別。 **一個定義按鈕class的範例** ```python= class test(discord.ui.View): def __init__(self): super().__init__(timeout=None) # 繼承父類的屬性 @discord.ui.button(label='test1') # 第一個按鈕 async def button_test(self, interaction: discord.Interaction,button: discord.ui.Button): await interaction.response.send_message('active') # 按鈕的互動回應 @discord.ui.button(label='test2') # 第二個按鈕 async def button_test1(self, interaction: discord.Interaction,button: discord.ui.Button): await interaction.response.send_message('not active') # 按鈕的互動回應 ``` Discord按鈕一排最多能有五個, 定義好 `test` 類後呼叫的方法與embed相同, 只需要在`send()` `send_message()` `edit_message()`等函式中傳入 `view=test()`。 **一個呼叫Button的範例** ```python= await interaction.response.send_message(content=None,view=test()) ``` ### 下拉式清單(Menu) ![](https://i.imgur.com/IvbRSyN.png =300x230) 下拉式清單提供一個多種選項的清單供使用者選擇, 並且做出不同的回應, 與Button屬於在相同的父類, 因此使用Menu時與Button一樣需要繼承 `discord.ui.View` 。 **一個定義下拉式清單class的範例** ```python= class Menu(discord.ui.View): def __init__(self, a, b, *args, **kwargs): # 初始化並傳遞參數 super().__init__(*args, **kwargs) # 繼承父類 self.a = a self.b = b @discord.ui.select(placeholder='choose',options[discord.SelectOption(label='1'),discord.SelectOption(label='2')]) async def callback(self, interaction: discord.Interaction,select: discord.ui.Select): if select.values[0] == '1': await interaction.response.send_message(content=str(self.a)) elif select.values[0] == '2': await interaction.response.send_message(content=str(self.b)) ``` 定義好 `Menu` 類後呼叫的方法與embed相同, 只需要在`send()` `send_message()` `edit_message()`等函式中傳入 `view=Menu()` 如果需要傳遞參數給 `Menu`, 可以透過實體化物件把參數傳遞給`Menu`做處理。 **一個傳遞參數並呼叫Menu的範例** ```python= the_Menu = Menu('hello', 'world') await interaction.response.send_message(content='greet',view=the_Menu) ```