如何让 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

当有人对消息做出反应时,我如何让我的 discord.js 机器人添加角色?

Discord bot 添加并等待问答游戏的反应

Discord bot - 发送多条消息的 Js 事件监听器