How to fetch multiple channel histories concurrently? - python

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.

Related

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

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

How do I check which reaction inthe message is higher in Discord.py?

So basicly, I'm creating a command that let you vote for something and after 5 min it will check which is higher.
But the problem is I don't know how to do that.
Here is my code so far:
#client.command()
async def strongy_boi(ctx, boi):
if boi == "copper-golem-allay":
mess = await ctx.send(":heart: = copper golem :blue_heart: = allay")
await mess.add_reaction('❤️')
await mess.add_reaction('💙')
await asyncio.sleep(5)
#do code that check which is higher
One way you can do this is to access the message reactions through discord.Message.reactions and iterate through them, checking the discord.Reaction.count and comparing with the current highest. To check for reactions, however, a message needs to be cached, which can be done through await fetch_message(). Do view the revised code and further explanations below.
#client.command()
async def strongy_boi(ctx, boi):
if boi == "copper-golem-allay":
mess = await ctx.send("❤️ = copper golem 💙 = allay")
await mess.add_reaction('❤️')
await mess.add_reaction('💙')
await asyncio.sleep(5)
# new code starts here #
msg = await ctx.channel.fetch_message(mess.id) # 'Cache' the message
# create variables to save the highest reactions
highest_reaction = ""
highest_reaction_number = 0
# msg.reactions format:
# [<Reaction emoji='❤️' me=True count=2>, <Reaction emoji='💙' me=True count=1>]
for reaction in msg.reactions: # iterate through every reaction in the message
if (reaction.count-1) > highest_reaction_number:
# (reaction.count-1) discounts the bot's reaction
highest_reaction = reaction.emoji
highest_reaction_count = reaction.count-1
await ctx.send(f"{highest_reaction} wins with {highest_reaction_count} votes!")
Other Links:
Count reactions on a message - Stackoverflow
How do I count reactions on a message in discord py? - Stackoverflow
Reactions counting - Stackoverflow
Get a List of Reactions on a Message - Stackoverflow

Discord.py command that deletes messages doesn't work

I'm trying to make a command that deletes a number of messages I want, but it doesn't work.
I got this error:
'coroutine' object has no attribute 'delete'.
if message.content.startswith("!נקה"):
delete_count = 0
try:
value = message.content.split("!נקה ",1)[1] #gets the value of the messages I want to delete
value = int(value)
flag = 1
except:
flag = 0
msg = await message.channel.send("שכחת לכתוב כמה הודעות למחוק.")
await asyncio.sleep(5)
await msg.delete()
if flag == 1:
for i in range(value-1):
if True:
with open("messagesList.txt", "r") as json_file:
messagesList = json.load(json_file) #loads a list of the id of messages
message_id = messagesList[0]
msg = message.channel.fetch_message(message_id)
await msg.delete()
delete_count += 1
with open("messagesList.txt", "w") as json_file:
json.dump(messagesList, json_file)
else:
print("", end = "")
if delete_count != 0:
message = await message.channel.send(f"{delete_count}הודעות נמחקו בהצלחה.") #prints the messages successfully delete
await asyncio.sleep(3) #wait 3 seconds
await message.delete()
Are you trying to create something like a purge command? If you want to simply delete some amount of messages from the channel (without other operations), then try this:
#client.command()
async def clear(ctx, amount = 5): # you can set the default amount of messages that will be deleted (if you didn't specify the amount while running the command)
deleted = await ctx.channel.purge(limit=amount + 1) # adding one to remove "clear" command too
await ctx.send(f"Deleted {len(deleted)} messages") # sends how many messages were deleted
Commands in docs
The error should be here:
except:
flag = 0
msg = await message.channel.send("שכחת לכתוב כמה הודעות למחוק.")
await asyncio.sleep(5)
await msg.delete() #The problematic line.
msg is a coroutine, not a Message object. It doesn't have a delete attribute.
You can simply use the delete_after=time parameter:
except:
flag = 0
msg = await message.channel.send("שכחת לכתוב כמה הודעות למחוק.", delete_after=5)
Another way is to make the bot delete the message, as you've tried:
except:
flag = 0
msg = await message.channel.send("שכחת לכתוב כמה הודעות למחוק.")
await asyncio.sleep(5)
await bot.delete_message(msg)
It's explained in this post.

How can I get the number of messages sent by a user in a discord server and store it in a list [Discord.py]

I'm currently using this code to check the number of messages sent by a user but this approach is very slow, it is taking 1 - 2 min to calculate for each user
user = discord.utils.find(lambda m: m.id== j, channel.guild.members)
async for message in channel.history(limit = 100000):
if message.author == user:
userMessages.append(message.content)
print(len(userMessages))
is there any other fast approach to doing this?
Counting messages
You can use on_message event to count messages.
message_count = {}
#client.event
async def on_message(message):
global message_count
if message.guild.id not in message_count:
message_count[message.guild.id] = {}
try:
message_count[message.guild.id][message.author.id] += 1
except KeyError:
message_count[message.guild.id][message.author.id] = 1
client.process_commands(message)
And then use
member = something # specify member here
try:
count = message_count[member.guild.id][member.id]
except KeyError:
count = 0
# now `count` is count of messages from `member`
To get count of messages from member.
Note: Message count resets on your bot restart but this solution would work very quickly.
Database
Another way to do what you want is use any database to store message count from different members.

How to monitor both reactions and removal of reactions in the same function using Discord.py

I currently have a function which is polling a message for reactions and adding users to a list based on that using Discord.py. Here is the code below:
#commands.Cog.listener()
async def on_message(self, message):
editMessage = message
tankBoosters = []
healBoosters = []
dpsBooster = []
boosters = []
# we do not want the bot to reply to itself
if message.author != self.bot.user:
return
if len(message.embeds) > 0:
for embed in message.embeds:
if "Boost is Ready" in embed.fields:
return
else:
pass
for x in message.embeds:
if '<:tank:801416324306829312>' and '<:healer:801416334243921971>' and '<:dps:801416343848615947>' in x.description:
await message.add_reaction('<:tank:801416324306829312>')
await message.add_reaction('<:healer:801416334243921971>')
await message.add_reaction('<:dps:801416343848615947>')
embedToEdit = x
def check(reaction, user):
return str(reaction.emoji) in ['<:tank:801416324306829312>', '<:healer:801416334243921971>', '<:dps:801416343848615947>'] and user != self.bot.user
boosters = tankBoosters + healBoosters + dpsBooster
while len(boosters) != 4:
if len(boosters) != 4:
reaction, user = await self.bot.wait_for('reaction_add', check=check)
print(message.reactions)
if reaction.emoji.name == 'tank' and len(tankBoosters) == 0:
tankBoosters.append(user)
if reaction.emoji.name == 'healer' and len(healBoosters) == 0:
healBoosters.append(user)
if reaction.emoji.name == 'dps' and len(dpsBooster) < 2:
dpsBooster.append(user)
if len(tankBoosters) == 1 and len(healBoosters) == 1 and len(dpsBooster) == 2:
message = f"<:tank:801416324306829312> {tankBoosters[0].mention} \n <:healer:801416334243921971> {healBoosters[0].mention} \n <:dps:801416343848615947> {dpsBooster[0].mention} \n <:dps:801416343848615947> {dpsBooster[1].mention}"
embedToEdit.add_field(name="Boost is Ready", value=message, inline=False)
await editMessage.edit(embed=embed)
This is working fine, but what I need to do be able to do is remove users from the respective lists (tank, heal and dps) when a reaction is removed from the message.
I.e. a message is posted and 3 tanks, 2 healers and 6 DPS "sign up" to the message by posting reactions to the message and they are appended to their respective lists. Then 1 tank and 2 DPS "unsign" by removing their reaction to the message. I need to remove those users from the list when they remove their reaction. I have looked into using message.reactions[0].users() but according to the VS Code debug terminal, message.reactions[0].users() is <discord.iterators.ReactionIterator object at 0x011B5DF0>
, which I unfortunately don't know enough about python or discord to understand!
I think you can try this:
#bot.event
async def on_raw_reaction_remove(payload):
reaction = str(payload.emoji)
msg_id = payload.message_id
user_id = payload.user_id
guild_id = payload.guild_id
exists = db.exists(msg_id)
When someone remove his reaction, you know what reaction, the user ID...
For those interested, or frustratedly searching for answers on how to do this, I ended up changing my approach slightly. Instead of doing all the logic inside the on_message function, I removed that and used on_raw_reaction_add and on_raw_reaction_remove instead, and I am tracking users by storing them in global arrays which are emptied once certain conditions are met.
This does mean that the code can only handle looking at one post at a time, but thats all I need it to do for now :)

Categories

Resources