Как заставить диссон-бот асинхронно ждать реакции на несколько сообщений? - PullRequest
2 голосов
/ 08 мая 2019

tl; dr Как мой бот может асинхронно ожидать реакции на множественные сообщения?


Я добавляю команду "rock-paper-scissors (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 Сравните асинхронный сценарий с несинхроннымone:

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.: -)

1 Ответ

1 голос
/ 08 мая 2019

Вы должны иметь возможность использовать 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)
    ...
...