今天要來把剩下的功能都開發完吧!
進度圖沒有變化。
今天要來接著開發計算與定時提醒功能。
複習一下使用者故事。
設定指令去執行計算本身並不難,這個功能最大的挑戰,是這個計算時間很長,需要有一些提示避免誤以為 Discord BOT 出事了,並且也需要避免這個計算影響到其他功能。
這邊就用一個簡單的函數代表繁重的計算:
import time
def heavy_calculations():
for i in range(10):
print(i)
time.sleep(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("計算完成!")
最後執行完畢的效果就變成這樣:
依照上面的寫法,執行 heavy_calculation
的時候會卡住 event loop,導致計算期間 Discord BOT 是不會有任何反應的。這個期間下的其他指令必須等到計算完畢才會觸發。
上圖就是一個簡單的範例。我在計算中嘗試報線索 (正常使用時,不會這樣做),但卻要等到計算完成才會看到線索格式錯誤的訊息。
卡住 event loop 的另一個問題是,這會導致 Discord BOT 下線,會造成大家恐慌XD。大家會不知道現在到底是 Discord BOT 發生非預期錯誤,還是只是還是還在計算中。因此,我們需要換個寫法,讓這個耗時的計算不會卡住 event loop。
這個問題蠻經典的,有很多人提問,可以參考這個 issue 或是這個 Discussion。簡單來說,這邊需要使用 asyncio
的 loop.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 被卡住。
- 作為頻道成員,我可以下達指令,查看當前最新的計算結果。
這個功能就比較沒什麼特別的了,跟查看線索資訊的概念是一樣的。
@app_commands.command()
async def result(self, interaction: discord.Interaction):
"""查看計算結果"""
result = get_result()
await interaction.response.send_message(result)
有時候大家忙著上班 (?),會一個不小心忘記報線索,這時候如果有一個定時提醒的功能就很棒。這個功能是每天下午 1 點檢查一次,如果當天還有人沒有報線索,就會發訊息 Tag 對方。
- 作為頻道成員,我可以享有 Discord BOT 定時檢查所有人是否已報線索的功能,若有未報者,該成員會被 Tag 提醒。
- 作為頻道成員,我可以開啟或關閉 Discord BOT 的定時檢查功能。
既然是「定時」提醒,就需要使用到 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,只好調整一下時間。
但這個功能,我其實有點擔心會不會大家覺得太吵,所以想說還是要預留一個開關。
@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()
。
今天介紹了如何在不影響原本功能的情況下,去執行繁重的計算,也介紹了如何做到每日提醒的功能。
總算是介紹完所有功能了,明天再來補個完賽心得吧!