如何让 Discord 机器人异步等待对多条消息的反应?
How to Make a Discord Bot Asynchronously Wait for Reactions on Multiple Messages?
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)
由于目标是收听等待来自多个消息的反应,我尝试创建两个单独的 threads/pools。这是三个尝试:
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
...
但是恕我直言,非异步会导致糟糕的用户体验。 :-)
您应该能够使用 asyncio.gather
安排多个协程同时执行。正在等待 gather
等待所有这些完成并 returns 他们的结果作为列表。
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)
...
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)
由于目标是收听等待来自多个消息的反应,我尝试创建两个单独的 threads/pools。这是三个尝试:
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
...
但是恕我直言,非异步会导致糟糕的用户体验。 :-)
您应该能够使用 asyncio.gather
安排多个协程同时执行。正在等待 gather
等待所有这些完成并 returns 他们的结果作为列表。
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)
...