Issues with making a simple loading icon inside an async event loop - python

I'm making a bot for a discord server and have a function that takes a bit of time to run. I want to add a spinning loading icon next to the status message like this Doing something: <spinning icon>. It edits the original message to loop through these messages:
Doing something: \
Doing something: |
Doing something: /
Doing something: -
I tried using a separate thread to update the message like this:
async def loadingBar(ctx, message : discord.Message):
loadingMessage0 = "{0}: \\".format(message)
loadingMessage1 = "{0}: |".format(message)
loadingMessage2 = "{0}: /".format(message)
loadingMessage3 = "{0}: -".format(message)
index = 0
while True:
if(index == 0):
await message.edit(contents = loadingMessage0)
index = 1
elif(index == 1):
await message.edit(contents = loadingMessage1)
index = 2
elif(index == 2):
await message.edit(contents = loadingMessage2)
index = 3
elif(index == 1):
await message.edit(contents = loadingMessage1)
index = 0
farther down, the bot command that starts the process...
#bot.command()
async def downloadSong(ctx, url : str, songname : str):
#Other code that doesn't matter
message = await ctx.send("Downloading audio")
_thread = threading.Thread(target=asyncio.run, args=(loadingBar(ctx, message),))
_thread.start()
#Function that takes a while
#Some way to kill thread, never got this far
However, I get the error Task <Task pending coro=<loadingBar() running at bot.py:20> cb=[_run_until_complete_cb() at /Users/user/.pyenv/versions/3.7.3/lib/python3.7/asyncio/base_events.py:158]> got Future <Future pending> attached to a different loop. I'm new to async programming and the discord libraries; Is there a better way to do this and if not what am I doing wrong?

Firstly, you should add a delay between iterations inside the while loop, use asyncio.sleep for this.
Secondly - asyncio and threading doesn't really work together, there's also no point in using threading here since it defeats the whole purpose of asyncio, use asyncio.create_task to run the coroutine "in the background", you can asign it to a variable and then call the cancel method to stop the task.
import asyncio
async def loadingBar(ctx, message : discord.Message):
loadingMessage0 = "{0}: \\".format(message)
loadingMessage1 = "{0}: |".format(message)
loadingMessage2 = "{0}: /".format(message)
loadingMessage3 = "{0}: -".format(message)
index = 0
while True:
if(index == 0):
await message.edit(contents = loadingMessage0)
index = 1
elif(index == 1):
await message.edit(contents = loadingMessage1)
index = 2
elif(index == 2):
await message.edit(contents = loadingMessage2)
index = 3
elif(index == 1):
await message.edit(contents = loadingMessage1)
index = 0
await asyncio.sleep(1) # you can edit the message 5 times per 5 seconds
#bot.command()
async def downloadSong(ctx, url : str, songname : str):
message = await ctx.send("Downloading audio")
task = asyncio.create_task(loadingBar(ctx, message)) # starting the coroutine "in the background"
# Function that takes a while
task.cancel() # stopping the background task

Related

How can I run a while loop which edits a aiosqlite database but is not in a async function

if True:
import shortcuts.startupload
from shortcuts.startupload import *
import threading
import time
**def background():
while True:
if 1 == 1:
if 1 == 1:
gwtimes = await aiosqlite.connect('gwtimes.db')
async with gwtimes.cursor() as gwcursor:
await gwcursor.execute("UPDATE gwtimes SET time=time+50")
await gwtimes.commit()
async with gwtimes.cursor() as gwcursor:
await gwcursor.execute("SELECT time FROM gwtimes")
number = await gwcursor.fetchone()
print(number)**
def foreground():
(other code)
b = threading.Thread(name='background', target=background)
f = threading.Thread(name='foreground', target=foreground)
b.start()
f.start()
bot.run(hidden token)
Above is most of the whole thing
I need it to (in the background) add 50 to all values in the column every 1 second and then print the time so I can test its working, however I dont get how to use async and await with this as the error is saying:
gwtimes = await aiosqlite.connect('gwtimes.db')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: 'await' outside async function```

Django Websocket: Messages are all sent after loop is finished

I am trying to create a application, where i start the game with a countdown. For that I have a loop counting down. The problem is when i am awaiting my send loop, all messsages are sent after the loop is finished. When i am putting the function in a task it is sending instant but thats not what i want because I need to wait for the function to call another after.
Thats the the countdown function
async def init_starting(self):
if not self.lobby.status == "waiting":
print("Already Started")
return False
else:
print("Starting Countdown")
self.lobby.status = 'starting'
await self.save_lobby()
await self.send_status_message_with_status(status='starting', data={'has_started': self.lobby.has_started,
'host': self.host.username,
'players': self.players,
'lobby_code': self.lobby_code,
'countdown': 3})
countdown = 3
while countdown >= 0: # here are the messages not sent instant but after the loop is finished (with my current approach they are sent instant but i cant await it
countdown_data = {
'countdown': countdown
}
await self.send_status_message_with_status(status='starting', data=countdown_data)
countdown -= 1
await asyncio.sleep(1)
self.lobby.status = 'started'
await self.save_lobby()
await self.send_status_message_with_status(status='started', data={
'message': "Test"
})
return True
Here i am currently starting the task
if "command" in text_data_json:
command = text_data_json['command']
match command:
case 'start':
loop = asyncio.get_running_loop()
start_task1 = loop.create_task(self.init_starting()) # here i would like to await the task but when i do it they are not sent instant
case _:
print("Code not found")
Thanks for any help in advance!

How to fetch multiple channel histories concurrently?

So I'm trying to make a command for my Discord bot, where it will check every channel in a server and check the last message in each channel, and then send all the channels that start with the key variable.
async def starthistory(self, ctx, key, msg, num):
for channel in ctx.guild.text_channels:
async for message in channel.history(limit=1):
message_content = message.content.lower()
if len(message.embeds) > 0:
if len(message.embeds[0].title) > 0:
message_content = message.embeds[0].title.lower()
elif len(message.embeds[0].author) > 0:
message_content = message.embeds[0].author.lower()
elif len(message.embeds[0].description) > 0:
message_content = message.embeds[0].description.lower()
if message_content.startswith(key.lower()):
num += 1
msg += f"\n**{num}.** {channel.mention} - **{channel.name}**"
#startswith
#_list.command(name="starts_with",
aliases=["startswith", "sw", "s"],
brief="Lists all channels with message starting with <key>.",
help="Lists all channels with last message starting with the word/phrase <key>.",
case_insensitive=True)
async def _starts_with(self, ctx, *, key):
msg = f"Channels with last message starting with `{key}`:"
num = 0
wait = await ctx.send(f"Looking for messages starting with `{key}`...")
asyncio.create_task(self.starthistory(ctx=ctx, key=key, msg=msg, num=num))
if num == 0:
msg += "\n**None**"
msg += f"\n\nTotal number of channels = **{num}**"
for para in textwrap.wrap(msg, 2000, expand_tabs=False, replace_whitespace=False, fix_sentence_endings=False, break_long_words=False, drop_whitespace=False, break_on_hyphens=False, max_lines=None):
await ctx.send(para)
await asyncio.sleep(0.5)
await wait.edit(content="✅ Done.")
I want it to concurrently look at each channel's history so it doesn't take as long. Currently, my code doesn't change the already defined variables: num is always 0 and msg is always None.
How to concurrently look at each channel's history instead of one at a time?
asyncio.create_task(coro) creates an asynchronous task and runs it in the background. To allow your for loop to run asynchronously, where all the text channels are being processed at the same time, you should use asyncio.gather(coros) instead.
Here is the working code (I trimmed down your code to only the relevant parts):
#staticmethod
async def check_history(msgs, channel, key, semaphore):
async with semaphore:
async for message in channel.history(limit=1):
message_content = message.content.lower()
# trimmed some code...
if message_content.startswith(key.lower()):
num = len(msgs)
msgs += [f"**{num}.** {channel.mention} - **{channel.name}**"]
#_list.command()
async def _starts_with(self, ctx, *, key):
msgs = [f"Channels with last message starting with `{key}`:"]
tasks = []
semaphore = asyncio.Semaphore(10)
for channel in ctx.guild.text_channels:
tasks += [self.check_history(msgs, channel, key, semaphore)]
await asyncio.gather(*tasks)
if len(msgs) == 1:
msgs += ["**None**"]
msgs += [f"\nTotal number of channels = **{len(msgs)}**"]
msg = "\n".join(msgs)
print(msg)
Main points of note/why this works:
I used asyncio.gather() to await all of the check_history coroutines.
I used a asyncio.Semaphore(10) which will restrict the max concurrency to 10. Discord API doesn't like it when you send too many requests at the same time. With too much channels, you might get temporary blocked.
Usually, it's not recommended pass immutable objects (strs and ints) to an external function and attempt to change its values. For your use case, I believe the best alternative is to use a msg list then str.join() it later. This gets rid of num as well.

How to set a background timer, and stop a function when it finishes while still waiting for reactions

So I'm making some minigame using discord.py, and this is what I got:
asyncio.create_task(self.stream_message_timer(ctx, correct, total), name=f"stream message timer {ctx.author.id}")
while bad_count > 0:
done, pending = await asyncio.wait([
self.client.wait_for('reaction_add', check=lambda reaction, user: str(reaction.emoji) in number_emojis and reaction.message.id == discord_message.id and user == ctx.author),
self.client.wait_for('message', check=lambda m: m.author == self.client.user and m.content == f"Times up!\n{correct}/{total} tasks successful.")
], return_when=asyncio.FIRST_COMPLETED)
try:
result = done.pop().result()
except Exception as e:
raise e
for future in done:
future.exception()
for future in pending:
future.cancel()
if type(result) == discord.Message:
return False
else:
reaction = result
# process the reaction, edit a message
await ctx.send(f"You deleted all the bad messages!\n{correct+1}/{total} tasks successful.")
for task in asyncio.all_tasks():
if task.get_name() == f"stream message timer {ctx.author.id}":
task.cancel()
break
return True
async def stream_message_timer(self, ctx, correct, total):
await asyncio.sleep(5)
await ctx.send(f"Times up!\n{correct}/{total} tasks successful.") # message linked to delete_chat, change there if this is changed
return False
Basically, I'm trying to make some kind of 5 second background timer as I wait for reactions at the same time.
No, I am not looking for timeout=5
The code that I have here works, but its very hacky. I'm either waiting for a reaction from the user, or just waiting for the bot to message "Times up".
Is there a cleaner way to do this? I would like to have the timer start right before the while loop, and forcefully stop anything inside the loop and make the function return False right when the timer finishes
Also, if the function does stop, I still want some way to cancel the timer, and that timer only.
I've been using this method for quite some time and it's making my code very unorganized. Thanks.
Here’s some sort of example that is independent from discord.py:
import asyncio
import random
async def main():
asyncio.create_task(timer(), name="some task name")
# simulates waiting for user input
await asyncio.sleep(random.uniform(2, 5))
return True
async def timer():
await asyncio.sleep(5)
# somehow make this return statement stop the main() function
return False
asyncio.run(main())
wait(return_when=FIRST_COMPLETED) is the correct way to do it, but you can simplify the invocation (and subsequent cancellation) by using the return value of create_task:
async def main():
timer_task = asyncio.create_task(timer())
user_input_task = asyncio.create_task(asyncio.sleep(random.uniform(2, 5)))
await asyncio.wait([timer_task, user_input_task], return_when=asyncio.FIRST_COMPLETED)
if not timer_task.done():
timer_task.cancel()
if user_input_task.done():
# we've finished the user input
result = await user_input_task
...
else:
# we've timed out
await timer_task # to propagate exceptions, if any
...
If this pattern repeats a lot in your code base, you can easily abstract it into a utility function.

Discordpy, praw, blocking

So i'm writing discord bot on python, and i want to make background task which would check subreddit for new submissions and post it if there's certain flair. But when i'm trying to start my bot, which worked perfect before, it just wait for something and doesn't start. What should i do?
async def reddit_task():
await bot.wait_until_ready()
start_time = time.time()
reddit = praw.Reddit(different keys)
subreddit = reddit.subreddit('certain subreddit')
for submission in subreddit.stream.submissions():
if submission.created_utc > start_time:
if submission.link_flair_text == 'certain flair':
em = discord.Embed(title=submission.title+'\n'+submission.shortlink)
if len(submission.preview) > 1:
em.set_image(url=submission.preview['images'][0]['source']['url'])
await bot.send_message(discord.Object(id='my channel id'), embed=em)
else:
pass
if __name__ == "__main__":
for extension in startup_extensions:
try:
bot.load_extension(extension)
except Exception as e:
exc = '{}: {}'.format(type(e).__name__, e)
print('Failed to load extension {}\n{}'.format(extension, exc))
bot.loop.create_task(reddit_task())
bot.run(config.bot_beta_token)
I ran into this problem as well since subreddit.stream.submissions() is blocking the for loop. So I solved it by using an infinite loop and subreddit.stream.submissions(pause_after=0) so it returns None if there are no new posts then waits 60 seconds before checking again.
async def reddit_task():
await client.wait_until_ready()
start_time = time.time()
reddit = praw.Reddit("<client_stuff>")
subreddit = reddit.subreddit("<some_subreddit>")
submissions = subreddit.stream.submissions(pause_after=0)
while not client.is_closed:
submission = next(submissions)
if submission is None:
# Wait 60 seconds for a new submission
await asyncio.sleep(60)
elif submission.created_utc > start_time:
<do_stuff_with_submission>

Categories

Resources