iT邦幫忙

2024 iThome 鐵人賽

DAY 30
0
Python

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

[Day 30] 開發實戰 (四):功能實作 (下)

  • 分享至 

  • xImage
  •  

今天要來把剩下的功能都開發完吧!

進度

進度圖沒有變化。

今天要來接著開發計算與定時提醒功能。

使用者故事 (User Story)

複習一下使用者故事。

2. 計算最佳做法功能

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

3. 其他功能

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

2. 計算最佳做法功能

設定指令去執行計算本身並不難,這個功能最大的挑戰,是這個計算時間很長,需要有一些提示避免誤以為 Discord BOT 出事了,並且也需要避免這個計算影響到其他功能。

這邊就用一個簡單的函數代表繁重的計算:

import time

def heavy_calculations():
    for i in range(10):
        print(i)
        time.sleep(1)

(1) 開始計算

要用什麼方式觸發?

相較於昨天的線索,這邊需要思考一下該怎麼觸發比較合適。會有這樣的原因是,我希望使用者可以知道「已經開始計算」和「計算完畢了」,所以,一次觸發就要回傳兩次訊息。

但是,Interaction 只能回應一次,如果想要再繼續傳送就要使用其他作法 (後面會介紹),因此,這邊就不能使用 hybrid command,不然使用 slash command 就會造成錯誤。最後,我選擇使用 slash command 來觸發。

如果想要依舊有 hybrid command 的效果也不是不行,只是要分成兩個函數分開處理。

正在輸入...

  • 作為頻道成員,在 Discord BOT 計算時,我會看到「正在輸入…」的提示。

這邊只要使用 Day 16 介紹的 typing() 就可以了:

@app_commands.command()
async def exchange(self, interaction: Interaction):
    async with interaction.channel.typing():
        heavy_calculations()
    await interaction.response.send_message("計算完成!")

不過,其實我後來才發現有另一種選擇,就是使用 defer(),改成顯示「正在思考...」。先來看一下效果:

使用的方法如下:

@app_commands.command()
async def exchange(self, interaction: Interaction):
    await interaction.response.defer()
    heavy_calculations()
    await interaction.followup.send("計算完成!")

需要注意的是,如同前面所述,一個 Interaction 只能回應一次,而 defer() 也算一個回應,所以後面也要使用 followup。此外,後面的第一個 followup 的訊息,會取代原本「正在思考...」的位置。

所以,如果把程式碼改成這樣:

@app_commands.command()
async def exchange(self, interaction: Interaction):
    await interaction.response.defer()
    await interaction.followup.send("開始計算!")
    heavy_calculations()
    await interaction.followup.send("計算完成!")

最後執行完畢的效果就變成這樣:

計算到一半,Discord BOT 會下線

依照上面的寫法,執行 heavy_calculation 的時候會卡住 event loop,導致計算期間 Discord BOT 是不會有任何反應的。這個期間下的其他指令必須等到計算完畢才會觸發。

上圖就是一個簡單的範例。我在計算中嘗試報線索 (正常使用時,不會這樣做),但卻要等到計算完成才會看到線索格式錯誤的訊息。

卡住 event loop 的另一個問題是,這會導致 Discord BOT 下線,會造成大家恐慌XD。大家會不知道現在到底是 Discord BOT 發生非預期錯誤,還是只是還是還在計算中。因此,我們需要換個寫法,讓這個耗時的計算不會卡住 event loop。

這個問題蠻經典的,有很多人提問,可以參考這個 issue 或是這個 Discussion。簡單來說,這邊需要使用 asyncioloop.run_in_executor

loop = asyncio.get_running_loop()
await loop.run_in_executor(None, heavy_calculations)

此外,為了避免有人手滑(?)再度觸發一次,需要做個簡單的防護措施。

@app_commands.command()
async def exchange(self, interaction: Interaction):
    if self.calculation_running:
        await interaction.response.send_message("計算正在進行中,請稍後再試!")
        return
    
    self.calculation_running = True
    total = calculate_total_conditions()
    await interaction.response.defer()
    await interaction.followup.send(f"計算出共有 {total} 種情況!")
    await interaction.followup.send("開始計算!")

    async with interaction.channel.typing():
        loop = asyncio.get_running_loop()
        await loop.run_in_executor(None, heavy_calculations)

    await interaction.followup.send("計算完成!")

    self.calculation_running = False

效果如下:

可以看到,這次 Discord BOT 就是先回應線索格式不正確,才回應計算完成的,代表有成功避免 event loop 被卡住。

(2) 查看結果

  • 作為頻道成員,我可以下達指令,查看當前最新的計算結果。

這個功能就比較沒什麼特別的了,跟查看線索資訊的概念是一樣的。

@app_commands.command()
async def result(self, interaction: discord.Interaction):
    """查看計算結果"""
    result = get_result()
    await interaction.response.send_message(result)

3. 定時提醒

有時候大家忙著上班 (?),會一個不小心忘記報線索,這時候如果有一個定時提醒的功能就很棒。這個功能是每天下午 1 點檢查一次,如果當天還有人沒有報線索,就會發訊息 Tag 對方。

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

(1) 定時提醒

既然是「定時」提醒,就需要使用到 Day 12 所介紹到的 Task 功能。

# cogs/remind.py

# Set time at 13:00 in GMT+8 (which is 05:00 UTC)
alert_time = [
    datetime.time(hour=5),
]

class RemindCog(commands.Cog):
    def __init__(self, bot: commands.Bot):
        self.bot = bot
        self.daily_task.start()
    
    def cog_unload(self):
        self.daily_task.cancel()

    @tasks.loop(time=alert_time)
    async def daily_task(self):
        members_to_remind = daily_check_clues()
        channel = self.bot.get_channel(settings.clue_channel_id)

        for member in members_to_remind:        
            await channel.send(f"請 {member.mention} 提供線索!")

為了 Demo,只好調整一下時間。

(2) 開啟/關閉定時提醒

但這個功能,我其實有點擔心會不會大家覺得太吵,所以想說還是要預留一個開關。

@app_commands.command(name="toggle")
async def toggle_remind(self, interaction: discord.Interaction):
    """開啟或關閉每日提醒"""
    if self.daily_task.is_running():
        self.daily_task.cancel()
        await interaction.response.send_message("已停止每日提醒")
    else:
        self.daily_task.start()
        await interaction.response.send_message("已開始每日提醒")

這邊使用 .is_running() 來判斷是否正在運行,如果是,就使用 cancel() 立刻中止。反之,就使用 start() 再次啟動。

要終止 task 除了可以使用 cancel(),也可以使用 stop()。差別是,stop() 還會執行完當下的這一次,也就是說,還會再提醒一次。所以,這邊我選擇使用 cancel()

小結

今天介紹了如何在不影響原本功能的情況下,去執行繁重的計算,也介紹了如何做到每日提醒的功能。

總算是介紹完所有功能了,明天再來補個完賽心得吧!
/images/emoticon/emoticon12.gif


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

尚未有邦友留言

立即登入留言