Discord.py - Asyncio Timeout not working properly - python-3.x

Expected output and what my code for it is:
My bot is supposed to send a message, and then check if the user who sent the command reacted on that message with :arrow_left:, :arrow_right:, or :wastebasket: and if they did, it is supposed to change the "page" of the message. That part works fine, but I also want the message to timeout after 7 seconds of inactivity.
embed = await self.get_member_list(ctx, member, page)
msg = await ctx.send(embed=embed)
reactions = ["⬅️", "➡️", "🗑️"]
for x in r:
await msg.add_reaction(x)
await asyncio.sleep(.35)
def check(payload):
return str(payload.emoji) in reactions
done = False
page = 1
while not done:
try:
pending_tasks = [self.bot.wait_for('raw_reaction_add', timeout=7.0, check=check),
self.bot.wait_for('raw_reaction_remove', timeout=7.0, check=check)]
done_tasks, pending_tasks = await asyncio.wait(pending_tasks, return_when=asyncio.FIRST_COMPLETED)
for task in done_tasks: payload = await task
user = await commands.MemberConverter().convert(ctx, str(payload.user_id))
except asyncio.TimeoutError:
done = True
return
else:
if user == ctx.author:
if str(payload.emoji) == "🗑️":
return await msg.delete(delay=0)
if str(payload.emoji) == "⬅️":
if page == 1:
page = total_pages
else:
page -= 1
if str(payload.emoji) == "➡️":
if page == total_pages:
page = 1
else:
page += 1
await msg.edit(embed=await self.get_member_list(ctx, member, page)) #
However, I am facing the problem "Task exception was never retrieved" after running the code above.
Actual results:
Task exception was never retrieved
future: <Task finished name='Task-58' coro=<wait_for() done, defined at C:\Users\AppData\Local\Programs\Python\Python38\lib\asyncio\tasks.py:434> exception=TimeoutError()>
Traceback (most recent call last):
File "C:\Users\AppData\Local\Programs\Python\Python38\lib\asyncio\tasks.py", line 501, in wait_for
raise exceptions.TimeoutError()
asyncio.exceptions.TimeoutError
The problem:
Whenever 7 seconds of inactivity passes and it is supposed to timeout I get an error. I have tried to fix this by running asyncio.gather() but I am unfamiliar with asyncio and I am unsure how to use it properly.
done_tasks, pending_tasks = await asyncio.wait(pending_tasks, return_when=asyncio.FIRST_COMPLETED)
await asyncio.gather('raw_reaction_add', 'raw_reaction_remove', return_exceptions=True)
I have tried:
Checking for typos
Running except Exception and except asyncio.exceptions.TimeoutError instead of except asyncio.TimeoutError
Contemplating my sanity
Reading the asyncio documentation, specifically on asyncio.wait()
Making sure that my bot has all the permissions and intents it needs in discord
Using self.bot.wait_for() with raw_reaction_add and raw_reaction_remove inside of a tuple instead of asyncio.wait()

I ended up getting rid of the error message by doing this:
First I moved the timeout from bot.wait_for() to asyncio.wait()
pending_tasks = [self.bot.wait_for('raw_reaction_add', check=check),
self.bot.wait_for('raw_reaction_remove', check=check)]
done_tasks, pending_tasks = await asyncio.wait(pending_tasks, timeout=7.0, return_when=asyncio.FIRST_COMPLETED)
Then I got the payload from a pop instead of awaiting task
payload = done_tasks.pop().result()
Now after 7 seconds of inactivity it tries to pop from an empty set and raises a KeyError which I can catch using except KeyError:

Related

asyncio.wait not returning on first exception

I have an AMQP publisher class with the following methods. on_response is the callback that is called when a consumer sends back a message to the RPC queue I setup. I.e. the self.callback_queue.name you see in the reply_to of the Message. publish publishes out to a direct exchange with a routing key that has multiple consumers (very similar to a fanout), and multiple responses come back. I create a number of futures equal to the number of responses I expect, and asyncio.wait for those futures to complete. As I get responses back on the queue and consume them, I set the result to the futures.
async def on_response(self, message: IncomingMessage):
if message.correlation_id is None:
logger.error(f"Bad message {message!r}")
await message.ack()
return
body = message.body.decode('UTF-8')
future = self.futures[message.correlation_id].pop()
if hasattr(body, 'error'):
future.set_execption(body)
else:
future.set_result(body)
await message.ack()
async def publish(self, routing_key, expected_response_count, msg, timeout=None, return_partial=False):
if not self.connected:
logger.info("Publisher not connected. Waiting to connect first.")
await self.connect()
correlation_id = str(uuid.uuid4())
futures = [self.loop.create_future() for _ in range(expected_response_count)]
self.futures[correlation_id] = futures
await self.exchange.publish(
Message(
str(msg).encode(),
content_type="text/plain",
correlation_id=correlation_id,
reply_to=self.callback_queue.name,
),
routing_key=routing_key,
)
done, pending = await asyncio.wait(futures, timeout=timeout, return_when=asyncio.FIRST_EXCEPTION)
if not return_partial and pending:
raise asyncio.TimeoutError(f'Failed to return all results for publish to {routing_key}')
for f in pending:
f.cancel()
del self.futures[correlation_id]
results = []
for future in done:
try:
results.append(json.loads(future.result()))
except json.decoder.JSONDecodeError as e:
logger.error(f'Client did not return JSON!! {e!r}')
logger.info(future.result())
return results
My goal is to either wait until all futures are finished, or a timeout occurs. This is all working nicely at the moment. What doesn't work, is when I added return_when=asyncio.FIRST_EXCEPTION, the asyncio.wait.. does not finish after the first call of future.set_exception(...) as I thought it would.
What do I need to do with the future so that when I get a response back and see that an error occurred on the consumer side (before the timeout, or even other responses) the await asyncio.wait will no longer be blocking. I was looking at the documentation and it says:
The function will return when any future finishes by raising an exception
when return_when=asyncio.FIRST_EXCEPTION. My first thought is that I'm not raising an exception in my future correctly, only, I'm having trouble finding out exactly how I should do that then. From the API documentation for the Future class, it looks like I'm doing the right thing.
When I created a minimum viable example, I realized I was actually doing things MOSTLY right after all, and I glanced over other errors causing this not to work. Here is my minimum example:
The most important change I had to do was actually pass in an Exception object.. (subclass of BaseException) do the set_exception method.
import asyncio
async def set_after(future, t, body, raise_exception):
await asyncio.sleep(t)
if raise_exception:
future.set_exception(Exception("problem"))
else:
future.set_result(body)
print(body)
async def main():
loop = asyncio.get_event_loop()
futures = [loop.create_future() for _ in range(2)]
asyncio.create_task(set_after(futures[0], 3, 'hello', raise_exception=True))
asyncio.create_task(set_after(futures[1], 7, 'world', raise_exception=False))
print(futures)
done, pending = await asyncio.wait(futures, timeout=10, return_when=asyncio.FIRST_EXCEPTION)
print(done)
print(pending)
asyncio.run(main())
In this line of code if hasattr(body, 'error'):, body was a string. I thought it was JSON at that point already. Should have been using "error" in body as my condition in any case. whoops!

Make sure that message has been sent

Let's say you are sending a message through your bot to all servers.
If you do too many actions through the api at the same time, some actions will fail silently.
Is there any way to prevent this? Or can you make sure, that a message really has been sent?
for tmpChannel in tmpAllChannels:
channel = bot.get_channel(tmpChannel)
if(not channel == None):
try:
if(pText == None):
await channel.send(embed= pEmbed)
else:
await channel.send(pText, embed= pEmbed)
except Exception as e:
ExceptionHandler.handle(e)
for tmpChannel in tmpAllChannels:
channel = bot.get_channel(tmpChannel)
if(not channel == None):
try:
if(pText == None):
await channel.send(embed= pEmbed)
else:
await channel.send(pText, embed= pEmbed)
except Exception as e:
ExceptionHandler.handle(e)
finally:
await asyncio.sleep(1) # this will sleep the bot for 1 second
Never hammer the API without any delay between them. asyncio.sleep will put a delay between messages, and you will send all of them without failing.

discord.py prevent bot from reading message as a command

The bot asks the user to select a type. I have used the client.wait_for() function to get user input. The command prefix is '.' In case the user types a message starting with '.' , I do not want the bot to read it as a command and execute that command. How do I do that?
This is the code:
#client.command()
async def search(ctx):
try:
await ctx.send("Enter '"+selected_type[0]+"' or '"+selected_type[1]+"' to search for required type")
msg = await client.wait_for('message', timeout=10, check=lambda message: message.author == ctx.author)
selection = msg.content.title()
except asyncio.TimeoutError as e: #if user does not give input in 10 sec, this exception occurs
await ctx.send("Too slow")
except:
await ctx.send("Search failed.")```
Will something like this work for you?
if not msg.content.startswith('.'): selection = msg.content.title()

Trying to get my bot to kick members that spam and have more than 3 violations

I can get the bot to delete spam messages perfectly, I cannot get it to kick members who's violation count is over 3.
if message.author.name in logs:
if message.author.name in users is True:
return
else:
delta = message.created_at-logs[message.author.name].lastMessage
if(delta.seconds < timeout):
logs[message.author.name].violations += 1
await message.delete()
print("Spam Detected!")
print("In Channel:", message.channel)
print("Spammer:", message.author.name)
print("Message: " + message.clean_content)
print("Time Deleted:", str(datetime.datetime.now()), "\n")
name = message.author.name
if name in logs:
log = logs[name]
if log.violations > 3:
await discord.Member.kick(reason=None)
logs[message.author.name].lastMessage = message.created_at
else:
logs[message.author.name] = Log(message.created_at)
The await discord.Member.kick(reason=None) pulls an error of TypeError: kick() missing 1 required positional argument: 'self'.
I've also tried using await discord.Guild.kick(user=user, reason=None) with the same error.
Try doing await message.guild.kick(message.author).
(Note that gives an error if the message is not sent in a guild)
Basically it fetches the guild that the message was sent in with message.Guild, and kicks the person who sent the message (message.author).
The reason why discord.Member.kick(reason=None) did not work was because discord.Member was a type, not an object.
Doing message.author should have been the correct way.
(It would also make more sense since its saying "fetch the author of this message", rather than saying "fetch the member of this discord", given the fact that there are a lot of members on discord)
The following reason is the same for discord.Guild.kick(user=user, reason=None) not working.

Deleting specific messages that was sent by the bot and sent by the user. Discord.py rewrite

I'm having a lot of trouble with discord.py rewrite and its migration. I looked at the migrating to v1.0 site and it said to put in message.delete() and so I did but I realised that wasn't working so I put in ctx aswell. But that put it to an error. There are two commands with this error at the moment.
I have already tried putting the message into a variable.
#client.command()
async def clear(ctx, amount=100):
message = ctx.message
channel = ctx.message.channel
messages = []
await ctx.channel.purge(limit=int(amount+1))
mymessage = await channel.send('Messages deleted')
await ctx.message.delete(mymessage)
#client.command()
async def verify(ctx, *, arg):
print(ctx.message.channel.id)
print(ctx.message.author)
if ctx.channel.id == 521645091098722305:
role = await ctx.guild.create_role(name=arg)
await ctx.message.author.add_roles(role)
mymessage = await ctx.send('Done! Welcome!')
await ctx.message.delete(mymessage)
await ctx.message.delete(ctx.message)
I expected the output to delete the message. For the clear one it deletes it and then gives it back. for the verify one it just keeps it as the same and shows the error:
raise CommandInvokeError(exc) from exc
discord.ext.commands.errors.CommandInvokeError: Command raised an exception: TypeError: delete() takes 1 positional argument but 2 were given
Also my role giving verify sometimes goes 3 times. I have went into task manager and killed all of the python too but it still did that. Once when I was clearing it said Done! Welcome as well. If you can answer this question as well, I would be pleased! Thank you in advance.
Message.delete doesn't take any arguments. It's a method that you call on the message you want to delete. Change
await ctx.message.delete(mymessage)
await ctx.message.delete(ctx.message)
to
await mymessage.delete()
await ctx.message.delete()
#client.command(pass_context=True)
async def delete(ctx, arg):
arg1 = int(arg) + 1
await client.purge_from(ctx.message.channel, limit=arg1)
!delete 10 - delete the last 10 posts

Resources