iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0
Python

用 Python 打造你的 Discord BOT系列 第 28

[Day 28] 開發實戰 (二):專案架構與主程式

  • 分享至 

  • xImage
  •  

今天要來開始寫程式了!

進度

有了昨天列出來的使用者故事 (需求),接下來就是把它們實作出來了。今天會依序介紹專案架構、主程式,最後會開始實作一點簡單的功能。

使用者故事 (User Story)

複習一下昨天整理出來的使用者故事。

1. 線索管理功能

  • 作為頻道成員,我可以輸入線索,讓 Discord BOT 取得並儲存相關資訊。
  • 作為頻道成員,若我輸入的線索格式不正確,會收到錯誤提示,且該線索不會被記錄。
  • 作為頻道成員,我可以編輯線索,並且 Discord BOT 會更新該線索資訊並檢查其格式。
  • 作為頻道成員,我可以刪除線索,Discord BOT 會相應更新線索資訊。
  • 作為頻道成員,我可以下達指令,要求根據歷史訊息來更新已儲存的線索資訊。
  • 作為頻道成員,我可以下達指令,要求查看目前儲存的線索資訊。
  • 作為頻道成員,我可以為其他頻道成員設定線索。

2. 計算最佳做法功能

  • 作為頻道成員,我可以下達指令要求開始計算。
  • 作為頻道成員,當我要求開始計算時,Discord BOT 會檢查是否所有成員當天都已提交線索,若不是,則會彈出提示訊息詢問是否確定要進行計算。
  • 作為頻道成員,我可以在計算開始前查看本次計算的組合數量。
  • 作為頻道成員,在 Discord BOT 計算時,我會看到「正在輸入…」的提示。
  • 作為頻道成員,計算完成後,我可以在特定頻道中看到 Discord BOT 提供的結果。
  • 作為頻道成員,我可以下達指令,查看當前最新的計算結果。

3. 其他功能

  • 作為頻道成員,我可以享有 Discord BOT 定時檢查所有人是否已報線索的功能,若有未報者,該成員會被 Tag 提醒。
  • 作為頻道成員,我可以開啟或關閉 Discord BOT 的定時檢查功能。
  • 作為頻道成員,我可以查看 Discord BOT 的使用指南或教學。

4. 使用限制

  • 線索只能在特定頻道中提交。
  • 只有特定的頻道成員可以提交線索。

補充:「在頻道內輸入線索」簡稱為「報線索」

開始之前...

除了上面比較偏向功能面的需求之外,在實作上也希望有以下幾個要求:

  1. 使用 bot 指令框架
  2. 使用 Cog 把指令們從主程式拆出來,並進行適當分類
  3. 各個指令中與 Discord 比較無關的部分,拆出來移到另一個資料夾

專案架構

以下是我的專案架構:

Repo
├─cogs
│   ├─__init__.py
│   ├─clue.py        # 線索管理
│   ├─exchange.py    # 計算最佳做法功能
│   ├─remind.py      # 定時提醒
│   └─tutorial.py    # 使用指南與教學
├─utils
│   ├─__init__.py
│   ├─crud_clues.py  # 線索資訊的增刪查改
│   ├─calculate.py   # 計算最佳化做法
│   ├─optimize.exe   # 計算最佳化做法的主程式
│   ├─clues.txt      # 儲存線索資訊
│   └─result.txt     # 儲存計算結果
├─.env
├─example.env
├─main.py            # 主程式
├─players.json       # 紀錄 Discord ID 與遊戲暱稱
└─requirements.txt

其中,cogs資料夾內放的是個各類分類的指令。分成幾類:

  • 線索
  • 計算最佳化交換做法
  • 定時提醒
  • 使用教學

utils 資料夾內放的則是其他與 discord 本身無關的,例如計算最佳化結果的相關程式。

.envexample.env 就會是存放環境變數,例如伺服器 ID、報線索頻道的 ID 等。
player.json 紀錄的是 Discord ID 與暱稱的對照表,在結果呈現上會使用到。

主程式

接下來,來看一下主程式。

# main.py

from typing import Optional

import discord
from discord.ext import commands
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
    token: str
    guild_id: Optional[int] = None

class MyBot(commands.Bot):
    def __init__(
        self,
        *args,
        initial_extensions: list[str],
        guild_id: Optional[int] = None,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self.guild_id = guild_id
        self.initial_extensions = initial_extensions

    async def setup_hook(self) -> None:
        for extension in self.initial_extensions:
            await self.load_extension(extension)

        if self.guild_id:
            guild = discord.Object(self.guild_id)
            self.tree.copy_global_to(guild=guild)
            await self.tree.sync(guild=guild)

settings = Settings()
exts = [
    "cogs.clues",
    "cogs.exchange",
    "cogs.remind",
    "cogs.tutorial",
]
intents = discord.Intents.default()
intents.message_content = True

bot = MyBot(
    initial_extensions=exts,
    guild_id=settings.guild_id,
    command_prefix="",
    intents=intents,
)

bot.run(settings.token)

相較於之前的範例,這個稍微複雜了一些,讓我們來看一下這兩個部分:

  1. 環境變數
  2. 載入 Extension 與 sync 應用指令

環境變數

這邊我選擇使用 Pydantic 的 Pydantic Settings 來取得 .env 中的環境變數 (預設路徑就是 .env)。

from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    token: str
    guild_id: Optional[int] = None

settings = Settings()

之後就可以輕鬆地從 settings 中取得 .env 中的環境變數了,例如:settings.token

我個人蠻喜歡用 Pydantic 來取得環境變數,因為它可以幫忙做型別轉換、驗證、設定預設值。如果各位對於取得環境變數有其他比較習慣的做法,都可以進行替換。

載入 Extension 與 sync 應用指令

class MyBot(commands.Bot):
    def __init__(
        self,
        *args,
        initial_extensions: list[str],
        guild_id: Optional[int] = None,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        self.guild_id = guild_id
        self.initial_extensions = initial_extensions

    async def setup_hook(self) -> None:
        for extension in self.initial_extensions:
            await self.load_extension(extension)

        if self.guild_id:
            guild = discord.Object(self.guild_id)
            self.tree.copy_global_to(guild=guild)
            await self.tree.sync(guild=guild)

由於指令都是放在 cogs 資料夾內,所以找個時機點使用 load_extension,那些指令才會生效。另外,我偏好還是要逐一列出要載入的 extension (要稱為 cog 也可以),而不是整包資料夾都送進去。

應用指令也有類似的狀況,為了避免等太久才生效,需要找個時機 sync 到指定的伺服器。不過,還是有保留一點彈性,把它設為 Optional 的參數。

而上面這兩個步驟的最佳時機點就是 setup_hook (再次強調,絕對不是 on_ready)。

第一個功能

今天先從簡單的功能開始做,讓大家熟悉這個專案架構。而最簡單的功能就是:

  • 作為頻道成員,我可以查看 Discord BOT 的使用指南或教學。

這個比較簡單,就直接來看程式碼。

# cogs/tutorial.py

from discord.ext import commands


class TutorialCog(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot

    @commands.hybrid_command()
    async def tutorial(self, ctx: commands.Context):
        await ctx.send("使用教學")


async def setup(bot: commands.Bot):
    await bot.add_cog(TutorialCog(bot))

為了節省版面,之後就只會呈現中間 Cog 的部分

這個功能的第一直覺應該是使用 help 來觸發指令,但是 help 是預設指令,所以這邊改成選擇使用 tutorial 來當作觸發的指令。

首先,要思考的是,要用什麼關鍵字來當作指令?由於 help 是預設指令,所以這邊選擇使用 tutorial 來當作觸發的指令。

可以看到目前只有一個指令:tutorial

使用教學的內容?

至於「使用教學」的內容... 我目前還沒寫XD

考量到內容應該頗長,這時候可以考慮直接放一個網址就好 (e.g. HackMD 筆記),或者是使用嵌入式內容 (請參考 Day 13) 做一個重點說明,並附上詳細說明的連結。

@commands.hybrid_command()
async def tutorial(self, ctx: commands.Context):
    embed = discord.Embed(
        title="使用教學",
        url="https://ithelp.ithome.com.tw/users/20162280/ironman/7781",
    )
    embed.add_field(name="tutorial", value="查看使用教學", inline=False)
    embed.add_field(name="exchange", value="開始計算最佳做法", inline=False)
    await ctx.send(embed=embed)

效果如下:

因為是使用 Hybrid Command,所以會同時設定兩種指令。

小結

今天介紹了專案架構和主程式,並開始實作功能。明天會繼續實作其他功能~


上一篇
[Day 27] 開發實戰 (一):定義需求
下一篇
[Day 29] 開發實戰 (三):功能實作 (上)
系列文
用 Python 打造你的 Discord BOT31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言