如何让 Discord 机器人异步等待对多条消息的反应?
Posted
技术标签:
【中文标题】如何让 Discord 机器人异步等待对多条消息的反应?【英文标题】:How to Make a Discord Bot Asynchronously Wait for Reactions on Multiple Messages? 【发布时间】:2019-09-26 06:36:00 【问题描述】:tl;博士 我的机器人如何异步等待对多条消息的反应?
我正在向我的 Discord 机器人添加一个石头剪刀布 (rps) 命令。用户可以调用命令可以通过输入.rps
和一个可选参数来调用,指定一个用户来玩。
.rps @TrebledJ
当被调用时,机器人将直接发送消息 (DM) 调用它的用户和目标用户(来自参数)。然后,这两个用户对他们的 DM 做出反应,使用 ✊、???? 或 ✌️。
现在我正试图让它异步工作。具体来说,机器人将向两个用户(异步)发送 DM 并等待他们的反应(异步)。分步场景:
Scenario (Asynchronous):
1. User A sends ".rps @User_B"
2. Bot DMs User A and B.
3. User A and B react to their DMs.
4. Bot processes reactions and outputs winner.
(另见:注1)
由于目标是侦听等待来自多条消息的反应,因此我尝试创建两个单独的线程/池。以下是三种尝试:
multiprocessing.pool.ThreadPool
multiprocessing.Pool
concurrent.futures.ProcessPoolExecutor
不幸的是,这三个都没有成功。 (也许我实现了一些不正确的东西?)
以下代码显示了命令函数 (rps
)、辅助函数 (rps_dm_helper
) 以及三个(不成功的)尝试。这些尝试都使用了不同的辅助函数,但底层逻辑是相同的。为方便起见,第一次尝试已取消注释。
import asyncio
import discord
from discord.ext import commands
import random
import os
from multiprocessing.pool import ThreadPool # Attempt 1
# from multiprocessing import Pool # Attempt 2
# from concurrent.futures import ProcessPoolExecutor # Attempt 3
bot = commands.Bot(command_prefix='.')
emojis = ['✊', '????', '✌']
# Attempt 1 & 2
async def rps_dm_helper(player: discord.User, opponent: discord.User):
if player.bot:
return random.choice(emojis)
message = await player.send(f"Playing Rock-Paper-Scissors with opponent. React with your choice.")
for e in emojis:
await message.add_reaction(e)
try:
reaction, _ = await bot.wait_for('reaction_add',
check=lambda r, u: r.emoji in emojis and r.message.id == message.id and u == player,
timeout=60)
except asyncio.TimeoutError:
return None
return reaction.emoji
# # Attempt 3
# def rps_dm_helper(tpl: (discord.User, discord.User)):
# player, opponent = tpl
#
# if player.bot:
# return random.choice(emojis)
#
# async def rps_dm_helper_impl():
# message = await player.send(f"Playing Rock-Paper-Scissors with opponent. React with your choice.")
#
# for e in emojis:
# await message.add_reaction(e)
#
# try:
# reaction, _ = await bot.wait_for('reaction_add',
# check=lambda r, u: r.emoji in emojis and r.message.id == message.id and u == player,
# timeout=60)
# except asyncio.TimeoutError:
# return None
#
# return reaction.emoji
#
# return asyncio.run(rps_dm_helper_impl())
@bot.command()
async def rps(ctx, opponent: discord.User = None):
"""
Play rock-paper-scissors!
"""
if opponent is None:
opponent = bot.user
# Attempt 1: multiprocessing.pool.ThreadPool
pool = ThreadPool(processes=2)
author_result = pool.apply_async(asyncio.run, args=(rps_dm_helper(ctx.author, opponent),))
opponent_result = pool.apply_async(asyncio.run, args=(rps_dm_helper(opponent, ctx.author),))
author_emoji = author_result.get()
opponent_emoji = opponent_result.get()
# # Attempt 2: multiprocessing.Pool
# pool = Pool(processes=2)
# author_result = pool.apply_async(rps_dm_helper, args=(ctx.author, opponent))
# opponent_result = pool.apply_async(rps_dm_helper, args=(opponent, ctx.author))
# author_emoji = author_result.get()
# opponent_emoji = opponent_result.get()
# # Attempt 3: concurrent.futures.ProcessPoolExecutor
# with ProcessPoolExecutor() as exc:
# author_emoji, opponent_emoji = list(exc.map(rps_dm_helper, [(ctx.author, opponent), (opponent, ctx.author)]))
### -- END ATTEMPTS
if author_emoji is None:
await ctx.send(f"```diff\n- RPS: ctx.author timed out\n```")
return
if opponent_emoji is None:
await ctx.send(f"```diff\n- RPS: opponent timed out\n```")
return
author_idx = emojis.index(author_emoji)
opponent_idx = emojis.index(opponent_emoji)
if author_idx == opponent_idx:
winner = None
elif author_idx == (opponent_idx + 1) % 3:
winner = ctx.author
else:
winner = opponent
# send to main channel
await ctx.send([f'winner won!', 'Tie'][winner is None])
bot.run(os.environ.get("BOT_TOKEN"))
注意
1 将异步场景与非异步场景进行对比:
Scenario (Non-Asynchronous):
1. User A sends ".rps @User_B"
2. Bot DMs User A.
3. User A reacts to his/her DM.
4. Bot DMs User B.
5. User B reacts to his/her DM.
6. Bot processes reactions and outputs winner.
这并不难实现:
...
@bot.command()
async def rps(ctx, opponent: discord.User = None):
"""
Play rock-paper-scissors!
"""
...
author_emoji = await rps_dm_helper(ctx.author, opponent)
if author_emoji is None:
await ctx.send(f"```diff\n- RPS: ctx.author timed out\n```")
return
opponent_emoji = await rps_dm_helper(opponent, ctx.author)
if opponent_emoji is None:
await ctx.send(f"```diff\n- RPS: opponent timed out\n```")
return
...
但是恕我直言,非异步会导致糟糕的用户体验。 :-)
【问题讨论】:
所以在写这篇文章时,我意识到我可以只坚持一个线程/进程,发送两个 DM,然后修改check
函数为 bot.wait_for
(因为 wait_for 是流行的阻塞函数),然后使用 while 循环检查反应。尽管如此,我仍然希望社区提供意见。可能有一种更好的异步方式来分支单独的进程。
【参考方案1】:
您应该能够使用asyncio.gather
来安排多个协同程序同时执行。等待gather
等待它们全部完成并以列表的形式返回它们的结果。
from asyncio import gather
@bot.command()
async def rps(ctx, opponent: discord.User = None):
"""
Play rock-paper-scissors!
"""
if opponent is None:
opponent = bot.user
author_helper = rps_dm_helper(ctx.author, opponent) # Note no "await"
opponent_helper = rps_dm_helper(opponent, ctx.author)
author_emoji, opponent_emoji = await gather(author_helper, opponent_helper)
...
【讨论】:
您好,感谢您的回答!我假设这使用了尝试 1 中的async def rps_dm_helper
?
是的,但是如果您只想在 wait_for
调用中使用这种方法,也可以使用这种方法,因为这些实际上需要很多时间。
好吧,我已经测试了gather
,但在执行.rps
并对表情符号做出反应后,我得到了回溯。我将使用完整的回溯编辑我的问题。 |我已经编辑了我的问题。
哎呀,在 20 次发布和部署之后,我想我明白了——我不得不await gather(...)
。
@TrebledJ 对不起,这是一个错字。如果你有领带,你至少应该浏览一下我为协程和任务链接的文档。正确使用它们可以产生一些非常简洁、强大的代码。以上是关于如何让 Discord 机器人异步等待对多条消息的反应?的主要内容,如果未能解决你的问题,请参考以下文章
如何让 discord bot 等待回复 5 分钟然后发送消息?使用不和谐的 js
C# Discord如何等待带有前缀的消息,然后响应它[关闭]
异步函数 discord.js 中的 awaitMessages