Skip to content
Advertisement

How to Make a Discord Bot Asynchronously Wait for Reactions on Multiple Messages?

tl;dr How can my bot asynchronously wait for reactions on multiple messages?


I’m adding a rock-paper-scissors (rps) command to my Discord bot. Users can invoke the command can be invoked by entering .rps along with an optional parameter, specifying a user to play with.

.rps @TrebledJ

When invoked, the bot will direct-message (DM) the user who invoked it and the target user (from the parameter). The two users then react to their DM with either ✊, 🖐, or ✌️.

Now I’m trying to get this working asynchronously. Specifically, the bot will send DMs to both users (asynchronously) and wait for their reactions (asynchronously). A step-by-step scenario:

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.

(See also: Note 1)

Since the goal is to listen to wait for reactions from multiple messages, I tried creating two separate threads/pools. Here were three attempts:

  • multiprocessing.pool.ThreadPool
  • multiprocessing.Pool
  • concurrent.futures.ProcessPoolExecutor

Unfortunately, all three didn’t work out. (Maybe I implemented something incorrectly?)

The following code shows the command function (rps), a helper function (rps_dm_helper), and the three (unsuccessful) attempts. The attempts all make use of different helper functions, but the underlying logic is the same. The first attempt has been uncommented for convenience.

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"```diffn- RPS: {ctx.author} timed outn```")
        return

    if opponent_emoji is None:
        await ctx.send(f"```diffn- RPS: {opponent} timed outn```")
        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"))


Note

1 Contrast the asynchronous scenario to a non-asynchronous 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.

This wasn’t too hard to implement:

...
@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"```diffn- RPS: {ctx.author} timed outn```")
        return
    
    opponent_emoji = await rps_dm_helper(opponent, ctx.author)
    if opponent_emoji is None:
        await ctx.send(f"```diffn- RPS: {opponent} timed outn```")
        return

    ...

But IMHO, the non-async makes for bad UX. :-)

Advertisement

Answer

You should be able to use asyncio.gather to schedule multiple coroutines to execute concurrently. Awaiting gather waits for all of them to finish and returns their results as a list.

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)
    ...
User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement