Discord.py: Multiple multipage messages - python

EDIT:
The solution was to add an additional condition in the check function, I have posted the details in a follow-up answer.
Main Question:
I am new python and decided to learn it by creating a discord bot.
On user command, I was able to get the bot to send a message that can change pages on react following this design:
I want to make a multi-page help command using discord.py
I can change the pages of one message just fine, however, if the user calls the command multiple times, reacting to one of the bot sent messages change the pages for all messages that haven't timed out. What can I change in the code so that reacting to one message won't trigger page changes in the other ones?
Additional questions:
def check(reaction, user):
return user == ctx.author and str(reaction.emoji) in ["◀️", "▶️"]
# This makes sure nobody except the command sender can interact with the "menu"
I understand that wait_for will use this function as a check, but where and how are the check parameters being passed in? How could I add more parameters if possible?
reaction, user = await bot.wait_for("reaction_add", timeout=60, check=check)
In the documentation, I couldn't pin down the return value of the wait_for method, it looks like it changes under different circumstances.
We aren't passing in a message instance into the wait_for method, so how does the method know which message to wait for a reaction on?
What happens to the flow of the program if the check fails? It appears that statements under this line are executed only if check() returns true. But what happens when the check fails?

A better alternative than the method one you provided is the use of ext.menus (It's in beta, so it doesn't have any docs yet, to install it python -m pip install -U git+https://github.com/Rapptz/discord-ext-menus)
Example
from discord.ext import menus
class MyMenu(menus.Menu):
async def send_initial_message(self, ctx, channel):
return await channel.send(f'Hello {ctx.author}')
#menus.button('\N{THUMBS UP SIGN}')
async def on_thumbs_up(self, payload):
await self.message.edit(content=f'Thanks {self.ctx.author}!')
#menus.button('\N{THUMBS DOWN SIGN}')
async def on_thumbs_down(self, payload):
await self.message.edit(content=f"That's not nice {self.ctx.author}...")
#menus.button('\N{BLACK SQUARE FOR STOP}\ufe0f')
async def on_stop(self, payload):
self.stop()
# later
#bot.command()
async def menu_example(ctx):
m = MyMenu()
await m.start(ctx)
Unfornatelly I can't answer your first question, I'm not sure why's that happening, sorry.
Answering your additional questions:
wait_for takes the same arguments as the event, on_message takes message so the check will take message as the single argument (the method will also only return message). Adding more arguments it's pretty similar to the basics of decorators, we wrap the check in another outer function
def check(func): # The ADDITIONAL arguments we want to pass, in this example another function
def inner_check(reaction, user): # The actual check
return user == ctx.author and func(reaction, user)
return inner_check
# To use it Note how I'm calling the check an additional argument
reaction, user = await bot.wait_for('reaction_add', check=check(lambda r, u: return True), timeout=60.0)
Python decorator basics
Same as the first question
The bot waits for any reaction to be added, if the check function returns True it will return the values
If the check returns other than True the bot will wait till the function returns True or the timeout is over

Thanks to Łukasz Kwieciński for answering my additional questions. Because I learned that wait_for waits for any reaction to any message, I've added an additional condition to the check() function. Now each message is independent of one another.
message = await ctx.send(f"Page {cur_page}/{pages}:\n{contents[cur_page-1]}")
# getting the message object for editing and reacting
def check(reaction, user):
if reaction.message != message:
return false
# SOLUTION: Checks if the message reacted on is the same as the one the bot sent
return user == ctx.author and str(reaction.emoji) in ["◀️", "▶️"]
# This makes sure nobody except the command sender can interact with the "menu"
However, this fix creates a possible performance problem. For every bot message that hasn't timed out yet, reacting to any message on the server causes it to run check() that many times.

Related

Discord Python cog_check issue

New to coding. I want to prevent my bot from responding to itself/other bots, and I'm trying to invoke a cog_check but can't make it work properly. Help would be greatly appreciated.
class CommandsMisc(commands.Cog):
def __init__(self, client):
self.client = client
async def cog_check(self, ctx):
if ctx.author.bot: # also tried: if ctx.author == self.client.user:
await exit()
#commands.Cog.listener("on_message") # example cog command
async def print(self, message):
if '!guide' in message.content:
await message.reply(Guide_Text)
else:
pass
Since some people seem to lack reading comprehension skills, my question is how can I best utilize "cog_check" in Discord.py so that all subsequent commands/listeners in the cog will check whether the message.author is the bot and then won't execute if it is?
It seems like what you want is a custom command decorator. You can do this as follows:
def is_not_bot():
async def predicate(ctx):
return not ctx.author.bot
return commands.check(predicate)
You place this in your cog class, then on any command inside the cog you can use the decorator #is_not_bot()
What does this do, exactly? Well, we create a function which can be used as a command decorator (#is_not_bot()). Inside this, we create a function called predicate. This predicate will return True if the user is not a bot. We then call return commands.check(predicate) inside the parent function. This will allow us to use this as a decorator.
discord.py docs reference
what you can have is a on_message event, which can do checks on the message before processing the command, something like this
bot = commands.Bot(command_prefix="!", intents=discord.Intents.all())
# or if you made your subclass of Bot or its variants
class CustomBot(commands.Bot):
def __init__(self):
...
bot = CustomBot()
#bot.event
async def on_message(message):
if message.author.bot:
return # this is a bot, so we can early exit
# any other checks
await bot.process_commands(message)
exit() will just make your entire program close. cog_check should return a Boolean that indicates if the command should run or not. In your case, you don't return anything if it's okay, so it returns None which is falsey, so your check always fails.
Make it return True if it should succeed, and False if it should fail.
Edit
The ext.commands framework already prevents bots from running your commands, so this is not something you should even have to worry about.
The reason your cog_check still doesn't do anything is because you don't have any commands. You're using an on_message listener to manually parse messages, instead of creating commands. Your "example cog command" is not a command, it's just an event listener. Those are two very different concepts.
cog_check is only invoked when actual commands are run, not before dispatching events like on_message, so this doesn't have any effect here. If you parse commands in on_message, checks aren't used at all.
See the docs for ext.commands for how to create commands: https://discordpy.readthedocs.io/en/stable/ext/commands/index.html

Perform tasks when the user has responded to a recation, otherwise do nothing

I have a problem. I want that when the user writes a message, my bot should send a message. And once the user has responded to that message, the user should send further instructions.
I have the problem that when the user sends a message, the bot sends the message with the reactions, but as soon as the user sends a second message. The bot sends a message again.
The bot should therefore only send another message once the user has reacted. How can this be done?
I tried this with the responsed variable.
import asyncio
import discord
from discord.ext import commands
from datetime import datetime
class command(commands.Cog):
def __init__(self, bot):
self.bot = bot
#commands.Cog.listener()
async def on_message(self, message):
if str(message.channel.type) == "private":
if message.author == self.bot.user:
return
else:
# check if channel exists
if (channel is None):
responsed = True
if(responsed):
responsed = False
...
await message.author.send("Hello")
# add reactions
reaction, user = await self.bot.wait_for('reaction_add', check=check) # check reactions
if(reaction.emoji == '✅'):
responsed = True
else:
pass # do nothing
else:
await message.author.send("How are you?")
async def setup(bot):
await bot.add_cog(command(bot))
User sends a Message
1.1. check if user reacted
1.2 if a message is sent and not reacted then pass
Bot replies with Hello -> Wait for reaction
2.1 bot adds reaction
2.2 if user sends a second message 2 will started again. That should be forbidden.
User Reacts
Bot send How are you
The problem with what you're trying to do, is that discord.py, and the discord API in general, is designed to run a bot on multiple servers, on multiple channels, etc.
This means that, if you want to forbid a user sending a second message, you have to specify what you mean with that. Not twice on the same channel? Not twice for the same user? Should there be some form of a timeout?
I see you're currently working only in private channels (which you can also check with if isinstance(message.channel, discord.abc.PrivateChannel) if you want to do that properly) In that case, you might want to restrict it like "not if in the last x messages in the private channel." You can do this with another guard clause at start, here shown with the entire list you're using:
if not isinstance(message.channel, discord.abc.PrivateChannel):
return
if message.author == self.bot.user:
return
previous_messages = await message.channel.history(limit=10).flatten()
if any([(message.content == "Hello" and message.author == self.bot.user) for message in previous_messages]):
return
... # Rest of the function
I personally don't think it's the cleanest, but it will at least work. (if I didn't make a typo somewhere...)
If I can give you one do-something-else bit of advice: use buttons and views instead. Those have this type of callback functionality build-in. :-)
I hope that helps! :-)

Discord.py wait_for message... How to take the message content and the person who wrote the message?

I'm searching to write a shutdown-room command, that mute all the users in a specific room.
So, I have to get the user that wrote the message and the message content.
For now I have this, but in this case every message is monitored:
def shut_check(msg):
return msg.content
#client.command()
#commands.has_permissions(manage_messages=True)
async def shut_room(ctx):
await ctx.send("Please send me the id of the room that you want to shut down.")
content = await client.wait_for("message", check=check)
Now, I have the content of the message sent, but how can I verify if the author of the message is the ctx.author?
I have another request, can you explain to me what's the purpose of the pass_context=True in:
#client.command(pass_context=True)
It's simple logic, define the check function inside the command. Also your current check doesn't make any sense at all, it will always return a true-like value
#client.command()
async def shut_room(ctx):
def check(msg):
return msg.author == ctx.author
await ctx.send("Please send me the id of the room that you want to shut down.")
message = await client.wait_for("message", check=check)
content = message.content
Also when waiting for a message it doesn't return the content itself, but a discord.Message instance, I think you got confused with the check function.
Explaining the check parameter: the bot will wait for the desired event until the check function returns a truthy value.
EDIT:
Defining the check func outside the command
def check(ctx):
def inner(msg): # This is the actual check function, the one that holds the logic
return msg.author == ctx.author
return inner
# inside the command
message = await client.wait_for("message", check=check(ctx)) # Note that I'm calling it
I've wrapped the check function with another function so I can pass the Context argument.

discord.py how to get message from the user again?

in discord.py rewrite, I am trying to make a vote system. Voting could require spaces, like
!vote do this option or do that option
So I want to get 2 messages with the same user.
At First, I used #client.commmands(), but I think using on_message is going to be better, but any of them is okay.
I am thinking of this,
#client.event
async def on_message(ctx): #We only get ctx because it can contain spaces
userid = ctx.author.id
#client.event
....
So my question is, is there any function it will make it so you can get content from same user 2 times, and can you use #client.event in async def.
Any Solutions? Thanks.
You can achieve the desired functionality through 2 ways.
Save the last message a user sent.
Wait for a new option in the vote command
As the last thing is better. I will explain how to do that.
Step 1: Create your !vote command
#client.commmands()
async def vote(ctx):
# logic to do some things when someone votes
Step 2: Add logic that waits_for the options
We use a timeout on the wait_for, so it wont go on forever, because we use a timeout we need to catch the exception it raises. This is done with the try, except. We also use a while loop, because this enables us to receive as many options as possible. Note that the condition in the while loop can be changed.
#client.command()
async def vote(ctx):
# logic to do some things when someone votes
try:
# While the user inputs options
while True:
await __handle_vote_option_message(ctx)
except asyncio.TimeoutError:
# The user did not respond in time.
return
async def __handle_vote_option_message(ctx):
timeout_ = 10
message = await client.wait_for('message', check=lambda message: message.author == ctx.author,
timeout=timeout_)
if not __is_message_valid_vote_option(message):
# logic to handle incorrect vote options
else:
# Whatever you want to do with the option the user provided.
def __is_message_valid_vote_option(message):
# check if message is correct.
return message.content.startswith("option")
This way is in my opinion far better than filling the on_message event with this kind of logic. As the logic belongs to the vote command not the on_message event.

Discord.py Extract user from reaction

I am writing a feature in my bot which allows 2 users to duel each other, and the way it works is that the bot sends a message telling everyone to get ready and then after a random number of seconds pass the bot reactions with a certain emoji on the message and the first person in the duel reacts to the message wins. It a simple I idea but I can't figure out how to see if a certain user has reacted to the message. My question is - is this possible or do I have to figure out another approach to this?
This is definitely possible with wait_for which is documented here.
The wait_for function waits for any specified event listed in the event reference.
It takes in three parameters Event, Check (optional) and timeout(optional)
there are examples in the documentation but for you it would be something like:
bot = commands.Bot(command_prefix='.', case_insensitive=True)
#bot.command()
async def game(ctx):
# Whatever you want here
msg = await ctx.send("React first to win")
await msg.add_reaction('👍')
def check(reaction, user):
return str(reaction.emoji) == '👍' and user != bot.user
try:
# Timeout parameter is optional but sometimes can be useful
reaction, user = await bot.wait_for('reaction_add', timeout=30, check=check)
# Will wait until a user reacts with the specified checks then continue on with the code
await ctx.send(f"Congratulations {user.name} you won!")
except asyncio.TimeoutError:
# when wait_for reaches specified timeout duration (in this example it is 30 seconds)
await ctx.send("You ran out of time!")
The Timeout=30 parameter is 100% optional you can remove it so it would wait forever (or until you turn your bot off). The check parameter is also optional.
P.S. I am assuming you are using python 3 or more hence the F-strings
Yes, that's possible for sure, there are multiple ways for doing this, I never tried it before since I didn't create any bots with reaction things but that should work.
using on_reaction_add
#client.event
async def on_reaction_add(reaction, user):
channel = client.get_channel('123')
if reaction.message.channel.id != channel.id:
return
else:
if reaction.emoji == "✅":
#Do something
print(user.name)
print(user.discriminator)
await channel.send(f"{user} has won the game")
Make sure to take a look at the on_reaction_add documentation and the reaction one

Categories

Resources