如何使不和谐的Bot异步等待对多条消息的反应?

时间:2019-05-08 13:43:23

标签: python asynchronous discord discord.py discord.py-rewrite

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)和3次(失败)尝试。所有尝试都使用不同的辅助函数,但是基本逻辑是相同的。为了方便起见,第一次尝试没有注释。

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

    ...

但是恕我直言,非异步会导致不良的UX。 :-)

[2]:其他上下文:该机器人托管在Heroku上,并与Python 3.7一起运行。

1 个答案:

答案 0 :(得分: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)
    ...