Carry bots tasks out over restarts and shutdowns - python

I have a simple python bot within discord.
Lets say my code is as follows:
#client.command(pass_context=True)
async def test(ctx):
await asyncio.sleep(500)
await ctx.send("Hello!")
This code will have the bot wait 500 seconds before sending "Hello! in chat. If I shutdown my bot and restart it, this timer will obviously be lost and unsaved.
How can I carry this function out across restarts and such?

The way I implement something like this is with a try-finally loop, the finally part executes even if you stop/restart your bot (not with every method of killing the process)
try:
client.run(TOKEN)
finally:
save('data.json', data)
To make this work you would expand your command that requires restart with a part that saves to a global dictionary/list. Something like (hellosave is the global list here):
hellosave.append((time.time() + 500, ctx.channel.id))
await asyncio.sleep(500)
await ctx.send("Hello!")
hellosave.pop(0)
As save() you would take that global dict/list and save it in some format (you can do it any way you'd like, from pickling to printing it in a .txt file [I chose json here cuz I needed it to be readable, that might not be the case in your case])
Next time you boot up your bot, in the on_ready() event you look for the contents of that file and restart the function based on the data saved in it, like for example your hello case you'd check if the time in the saved event has already passed, if it has you send it immediately (or not at all) if it hasnt you create a task with asyncio.create_task that waits for the amount it needs (time.time() - hellotime) [to make this work you have to save the channel id, like I did in my example]

Related

How do I gracefully handle Ctrl + C and shutdown Discord Py bot?

I'm using the Discord.py library to make a python discord bot, and on shutdown or when it gets SIGINIT I want it to run a shutdown handler (update database, close database connections, etc) I have the main python script and also discord cogs that I want to cleanly shutdown. A simple google just says to use a Try/Finally, and just to have it run client.close() but it needs to be awaited. So it looks something like this:
try:
bot.run(config["token"])
finally:
shutdownHandeler()
bot.close()
but it wants me to await the bot.close coroutine except I cant do that if its not inside an async function. I'm pretty new to asyncio and discord py so if anyone could point me in the right direction that would be awesome! Thanks in advance for any help.
The issue with run() is that you can't just use try - finally to implement cleanup, because during the handling for run(), discord.py already closed the event loop along with the bot itself. However, during the first phase of run()'s cleanup, the bot.close() method gets called. So it would be best to run your cleanup operations in that close method:
from discord.ext import commands
class Bot(commands.Bot):
async def async_cleanup(self): # example cleanup function
print("Cleaning up!")
async def close(self):
# do your cleanup here
await self.async_cleanup()
await super().close() # don't forget this!
bot = Bot(None)
bot.run(TOKEN)
Why not declare bot.close() and shutdownHandeler() in an async function then call it when it's necessary, in your finaly or in a stop command or anything else you would think of ?

"on_message" of Discord.py can't work after I implemented "schedule" into it

I'm not familiar with Python so I'm afraid that this could be a silly question for all of you but I have already tried my best to solve it.
I worked on a self-project of making a bot to do some silly task just for fun.
First, I have done a function to let it receive a message with "on_message" event, which works well. here's the code
import discord
import os
discordClient = discord.Client()
#discordClient.event #event register
async def on_ready():
print('Logged in as {0.user}'.format(discordClient))
await routineAnnouncer.createDisocrdRoutine(discordClient)
#discordClient.event
async def on_message(message):
checked = False
if message.author == discordClient.user:
checked = True
return
else:
print("Get Message {0.content} from {0.author.name} ({0.author.id}) #{0.channel.name} ({0.channel.id})".
format(message))
#-------------------------- RUN ----------------------
print('Registering EVENTS COMPLETE')
discordClient.run(os.environ['discordToken'])
I code it on Repl.it and "os.environ" is its way to retrieve .env.
here are the code in "routineAnnouncer" that is used in the "on_ready" event
import os #ใช้ดึง .env (secret)
import schedule
import time
def scheduleAnnouncer(discordClient,announceType,announceDetail):
print("Announcing {} {}".format(announceType,announceDetail))
lzChannel = discordClient.get_channel(int(os.environ['lz_token_test']))
discordClient.loop.create_task(lzChannel.send("Announcing {} {}".format(announceType,announceDetail)))
async def createDisocrdRoutine(discordClient):
print("Registering CRONJOBS...")
scheduleAnnouncer(discordClient,"headerTest","descTest")
schedule.every(5).seconds.do(scheduleAnnouncer,discordClient,"header1","desc1")
schedule.every(10).seconds.do(scheduleAnnouncer,discordClient,"header2","desc2")
# while True:
# schedule.run_pending()
# time.sleep(5)
So, it supposed to do createDisocrdRoutine() after the connection is ready and set the scheduling text.
As you can see the last section of the code is commented. It is supposed to be a loop that triggers the scheduled text to be sent via discord to a designated channel.id.
To this point. The code works fine. the "on_message(message)" section able to print out whatever is sent to a channel.
The function "scheduleAnnouncer" also works fine. It can send message to the channel.
Discord Screen Repl.it Screen
After a loop "while: True" is uncommented in order to let the schedule work.
The loop works fine. It prints out the text as shown in the loop. but discord can't detect any text that is sent to the same channel before. Even the function "scheduleAnnouncer" that supposed to send a message is broken. It feels like anything involved with discord is broken as soon as the "while: True" is uncommented.
Discord Screen Repl.it Screen
I tried to separate the scheduling into another thread but it didn't work. I tried to use other cronjob managements like cronjob, sched or something else. Most of them let me face other problem.
I need to send argument into the task (discordClient, announceType, announceDetail).
I need to use the specific date/time to send. Not the interval like every 5 seconds. (The text that needs to be sent differ from time and day. Monday's text is not that same as Friday's text)
From both criteria. The "schedule" fits well and suppose to works fine.
Thank you in advance. I don't know what to do or check next. Every time I try to solve it. I always loop back to "schedule" and try to works with it over and over.
I hope that these pieces of information are enough. Thank you so much for your time.
time.sleep() is not async so it will freeze your program. Use await asyncio.sleep(). This is because it is pausing the whole program, not just that function.
The reason you’d want to use wait() here is because wait() is non-blocking, whereas time.sleep() is blocking. What this means is that when you use time.sleep(), you’ll block the main thread from continuing to run while it waits for the sleep() call to end. wait() solves this problem.
Quote From RealPython

How to use Context within discord.ext.tasks loop in discord.py?

So what I've been trying to do is to create a task that collects data from API and sends it to specific channel in the guild every day at the same time.
#tasks.loop(minutes=60)
async def deals(self):
if datetime.now().hour == 12:
session = aiohttp.ClientSession()
request = await session.get('url')
deals_list = await request.json()
embeds_list: list = []
# ...
# Here is code responsible for assembling the embeds list to send from gathered data, which is irrelevant
# ...
for guild in self.bot.guilds:
try:
category = discord.utils.get(guild.categories, name='category')
channel = discord.utils.get(guild.channels, name='channel', category=category)
await channel.purge()
for e in embeds_list:
await channel.send(embed=e)
except discord.errors.Forbidden:
continue
except discord.errors.NotFound:
await create_missing_channels(guild) # my function
So as you can see I wanted to make this task run on every guild that the bot is connected to. The problem is that without the Context, I need to use the loop to iterate through every guild and execute the same code on each of them, which is probably not the most efficient solution. If I were to run this on 100 guilds at the same time, it would take ages.
Is there any possible way to use Context within task, or do you see any other possibility to handle task as this one without iterating over each guild to make it more efficient?
No, there is no Context in a Task. What you could do, however, is iterate through all guilds on startup and store what you need somewhere in your bot as a botvar. You could run this as a task that only executes once when it starts:
self.bot.channel_dic = {}
for guild in self.bot.guilds:
try:
category = discord.utils.get(guild.categories, name='category')
channel = discord.utils.get(guild.channels, name='channel', category=category)
self.bot.channel_dic[guild.id] = channel.id
except discord.errors.NotFound:
await create_missing_channels(guild)
Now you have a dictionary mapping every guild's id to their corresponding Channel that you wanted to send a message to, meaning in the future you can just iterate over those instead of having to use utils.get twice for each one to find them all again.
Do remember to also add the channel created in the create_missing_channels function into the dictionary in case it didn't exist, otherwise it'll be missing.
for guild in self.bot.channel_dic:
channel = self.bot.channel_dic[guild]
try:
await channel.purge()
for e in embeds_list:
await channel.send(embed=e)
except discord.errors.Forbidden:
continue
Ok, so my problem was that I was iterating over guilds, so whevener the task was going to start, it would execute the code on each guild separately, one by one, which would make it take a long time if there were a lot of guilds to handle.
If anyone was looking for the possible solution, I managed to solve it with asyncio. I put the logic code in separate function and created new task loop that handles it with asyncio.gather():
#tasks.loop(minutes=60)
async def deals(self):
if datetime.now().hour == 12:
coroutines = [deals_task(guild) for guild in self.bot.guilds]
await asyncio.gather(*coroutines)
I might have misunderstood this.
But if for every guild(server) your bot is in you create a class that is separate from discord.py but still using the functionality of it. (Don’t rely on discord.py classes, use them in your application instead.). create a function in each classinstance for your loop.
Then you iterate through the class of instances and start a loop for each that fits the description.
Create a decorated loop function in the class
GlobalGuildClass[guild].loop()
For gid in GlobalGuildClass
GlobalGuildClass[gid].loop.start()
Then on_ready() iterate through GlobalGuildClass and start the loop for each.
?
I don’t know if it works. but in theory it could work.
Perhaps not pythonic.

Async timer function blocks itself when called twice

I'm currently trying to implement a function to my Discord Bot where I can easily delete the sent message after a set amount of seconds.
Here is my function:
async def messageCountdown(context, message, counter):
response = await context.send(f"**{'—' * counter}** \n {message}")
for i in range(counter, 0, -1):
await response.edit(content=f"**{'—' * i}** \n {message}")
await asyncio.sleep(1)
await context.message.delete()
await response.delete()
Function call:
#client.command()
async def test(context, *message):
await messageCountdown(context, "Test", 10)
The function itself runs totally fine if only called once:
https://gyazo.com/3b1eef9ecf8ecbe6473e8b20dfcd19d1
As soon as I call it twice or more often, the countdown goes down inconsistently in a weird way: https://gyazo.com/af4b23c5831ae90d5bc5a8461a22b0d7
I tried the same again but replaced await asyncio.sleep(1) with time.sleep(1), same result.
This is where I don't know how to continue, as all I found was that asyncio should solve the problem, which it obviously doesn't. Also, I don't understand why one function blocks the opposite function since neither asyncio nor time should do so as the function is asynchronous (which should exactly prevent what happens right now, shouldn't it?).
There is no problem with the async function here. The problem is Discord API Rate Limits
You call the function twice so it does edit message twice every second instead of once.
Once you hit the rate limit the bot does the thing but it isn't yet updated due to rate limit, as soon as the rate limit is removed it instantly updates the message which causes it to jump from step 3 to step 1 directly, resulting in inconsistent updation of message.
Read More about Discord API Rate Limits: here

A scheduled task to a discord bot in python fails to do its job

I am programming a discord bot in python. It needs to sweep the member list every 24hrs, check their roles and do some actions accordingly. I started to program this, but the scheduled task apparently doesn't have access to discord. I can't seem to get a member. When I do this in a command:
#bot.command(name='sweepercmd', help='')
async def sweepercmd(ctx):
member = get(bot.get_all_members(), name="Waldstein")
print(member)
It prints "Waldstein#4164" in the bash console as expected. However if I put the same code in a task like this:
#tasks.loop(hours=24.0)
async def sweeper():
member = get(bot.get_all_members(), name="Waldstein")
print(member)
It prints "None". Adding ctx like ...sweeper(ctx)... makes it hang.
How can I access discord within my task just like in the commands?
thanks in advance for the help,
Jo
As for why your tasks are failing, I'm not entirely sure, but I'll look into it for you and edit my answer when I get some info on it. In the mean time, this should suffice:
You can use loop.create_task() as an alternative for this.
async def sweep_task():
while True:
member = discord.utils.get(bot.get_all_members(), name="Waldstein")
print(member)
await asyncio.sleep(86400)
#bot.command()
async def sweep(ctx): # doesn't necessarily need to go into a command - you could put it into
# your on_ready event if you want to
bot.loop.create_task(sweep_task())
References:
Client.loop
Client.get_all_members()
utils.get()
loop.create_task()
asyncio.sleep()

Categories

Resources