Discord.py: Waiting for one on_raw_reaction_add to finish before completing another - discord

I am creating a movie bot on discord that selects 10 random movies, members vote on it, and the winning movie (random in case of tie) gets a discussion thread created. All of this is done through scheduled functions.
I recently switched from doing ✅ on every movie summary, to a poll message with keypad numbers 1️⃣,2️⃣,3️⃣,4️⃣,5️⃣,6️⃣,7️⃣,8️⃣,9️⃣,🔟. Users react with the number to the corresponding movie. Voting works, but I am trying to make it so users can only vote on one movie for each poll.
This is done via on_raw_reaction_add, the issue is, if a user spams reactions, the logic has potential to remove the latest reaction depending on how fast a user selects it. I need some way to have on_raw_reaction_add to wait for the first event to complete before handling the next reaction add.
I took a snippet I saw from this answer from Diggy.
#Check for Duplicate Reaction
#bot.event
async def on_raw_reaction_add(payload): # checks whenever a reaction is added to a message
# Check if the reaction was made by the bot, and if the reaction was done in the voting channel.
if (payload.user_id != bot.user.id and payload.channel_id == vChannel):
#Set channel and message
channel = await bot.fetch_channel(payload.channel_id)
message = await channel.fetch_message(payload.message_id)
# iterating through each reaction in the message
for r in message.reactions:
# checks the reactant isn't a bot and the emoji isn't the one they just reacted with
if payload.member in await r.users().flatten() and not payload.member.bot and str(r) != str(payload.emoji):
# removes the reaction
print('Removing the reaction' + r.emoji)
await message.remove_reaction(r.emoji, payload.member)
else:
print(str(datetime.datetime.now()) + r.emoji + ': not removing')
This removes duplicate reactions as intended, if the user goes slow enough. If the user clicks multiple reactions quickly, the bot loops though and may remove a more recent reaction, instead of keeping the latest one.
I tried to see how to use wait_for but couldn't figure out how to do it.
The best way I have thought to visually represent this is a table:
Reaction 2
Reaction 3
Reaction 5
Add Reaction 2
Check for 1, remove
Add Reaction 3
Check for 2, keep
Check for 1, remove
Add Reaction 5
Check for 3, remove
Check for 2, remove
Check for 1, remove
Check for 4, remove
Check for 3, keep
Check for 2, remove
Check for 5, remove
Check for 4, remove
Check for 3, remove
Check for 6, remove
Check for 5, remove
Check for 4, remove
Check for 7, remove
Check for 6, remove
Check for 5, keep
Check for 8, remove
Check for 7, keep
Check for 6, remove
Check for 9, remove
Check for 8, remove
Check for 7, remove
Check for 10, remove
Check for 9, remove
Check for 8, remove
Check for 10, remove
Check for 9, remove
Check for 10, remove
If you add 2️⃣,3️⃣,5️⃣ quickly, depending on how long it takes to process the check for 2️⃣, it can remove the 3️⃣,5️⃣ reactions. In the example above, if a user reacts with 5️⃣ before the on_raw_reaction_add event for 3️⃣ checks for 5️⃣, 5️⃣ will be removed even though it was the latest reaction.
I need some way to ensure the check/loop completes in sequential order.
Video of the issue, sped up slightly.

The fetch_channel and fetch_message methods are api calls, which can be quite slow.
Rather use Partial Objects (partial messageable and partial message). You can build a cache (or a database). Store a list of user IDs who have voted against the message id of the poll.
In the reaction event, if the user id is already present for the message in the cache, remove the reaction.
bot.poll_cache = {} #Key is message id which you set while creating the poll, Value is a list of user ids. Set it to an empty list
#bot.event
async def on_raw_reaction_add(payload):
user_list = bot.poll_cache.get(payload.message_id)
if user_list is None: #not a poll
return
if not payload.member.bot and payload.member.id in user_list:
pmsg = bot.get_partial_messageable(payload.channel_id).get_partial_message(payload.message_id)
await pmsg.remove_reaction(payload.emoji, payload.member)
else:
user_list.append(payload.member.id)

Related

Discord.py Reaction Events

My bot sends an embed every time a new member joins. Then the bot adds the little 👋🏽 reaction to it. I want members to be able to welcome the new member by reacting. If they react, then they will be rewarded in some way. So onto my question, how would I make my bot watch for reactions for 60 seconds after the embed is sent, and what event would I use? I've found a few things in the documentation but none of which seem to make sense to me. A code example and explanation of how it works would be amazing. Thanks in advance!
In order to do this, you'd need to make use of the on_reaction_add event, assuming you already have all your imports:
#bot.event
async def on_member_join(member): # when a member joins
channel = discord.utils.get(member.guild.channels, name="welcome") #getting the welcome channel
embed = discord.Embed(title=f"Welcome {member.mention}!", color=member.color) #creating our embed
msgg1 = await channel.send(embed=embed) # sending our embed
await msgg1.add_reaction("👋🏽") # adding a reaction
#bot.event
async def on_reaction_add(reaction, user):
message = reaction.message # our embed
channel = discord.utils.get(message.guild.channels, name="welcome") #our channel
if message.channel.id == channel.id: # checking if it's the same channel
if message.author == bot.user: #checking if it's sent by the bot
if reaction.emoji.name == "👋🏽": #checking the emoji
# enter code here, user is person that reacted
I think this would work. I might have done the indentations wrong since I'm on another device. Let me know if there are any errors.
You can use the on_reaction_add() event for that. The 60 second feature might be complicated.
If you can get the user object from the joined user the on_reaction_add() event, you could check if the user joined less than 60 seconds ago.
You can check the time a user joined with user.joined_at, and then subtract this from the current time.
Here is how to check how many seconds a user is already on the server.
from datetime import datetime
seconds_on_server = (datetime.now() - member.joined_at).total_seconds()
A rather hacky solution is to retrieve the original user who joined through the message on which the reaction is added. Members have the joined_at attribute, which is a datetime object, with it you can just snap current datetime and subtract the former from it. The resulting is a timedelta object which you can use to calculate the time difference.

Discord.py Detecting a nickname change and then changing it back to the previous nickname

async def chnick(ctx, member: discord.Member, nick):
loop = True
while loop==True:
await member.edit(nick=nick)
time.sleep(5)
This is the code i have right now. It just loops the nickname change every 5 seconds which makes the bot run very slow. Is there a way you could make it so that it checks for a change in nickname and if it detects one then it sets it back to the previous one.
You can use the on_member_update(before, after) event.
More Information in the Docs

Discord, sending a message to a different category and channel depending upon which reaction is selected

I have been working on a way within my discord server to try to automate the flow of users who want to try out for a team using bots. Currently I have adopted YAGPDB and have a role menu created in a community level (category) #general_tryouts channel with reactions to specify which game they want to pursue. When they select a reaction they are assigned a temporary role (24 hours) which will grant them access to the game specific category #tryouts channel they've selected. I've been successful with getting all of the roles assigned using YAG's gui interface. However, I would also like for a message to be sent to the game specific #tryouts channel with an embed (similar to a welcome message) stating that they would like to tryout, to notify the 'team' that they are "in the queue". So the process would look something like this:
User A pops into (Some Community) #general-tryouts -> reads the menu asking them which team they want to tryout for and selects option 1 (':one:') -> User A is given the role for TempGame1 and can now see (Some Game) #tryouts AND simultaneously a message is sent to (Some Game) #tryouts on their behalf.
If they choose option 2 they will receive a TempGame2 role and a message should be sent to (Another Game) #tryouts.
If they choose option 3 they will receive a TempGame3 role and a message should be sent to (ThisOther Game) #tryouts.
YAGPDB has the option with it's custom commands to fire a command triggered by a reaction given in a certain channel by someone with a certain role. The issue is getting the result of which reaction they selected, and that dictating where the message is sent. I'm really not even concerned if it's just a generic message. I just want User A to be able to react at a 'community' level and a message to get passed in to (Some Game) #tryout, (Another Game) #tryouts, or (ThisOther Game) #tryouts based on their selection.
My apologies in advance. I am only proficient enough to code a simple I say "ping" you output "pong" sort of transaction, and got in a little over my head in short order. I have looked at the developer resources trying to piece something together to no avail, as well as pulling a similar snippet of code from here and attempting to modify it to suit my needs, also to no avail. Any help would be greatly appreciated.
Code Sample
if (reaction.message.id === '730000158745559122') {
let channel = reaction.message.guild.channels.cache.get(targetChannelId);
console.log(reaction.emoji)
if (channel) {
let embed = new Discord.MessageEmbed();
embed.setAuthor(
user.tag,
user.displayAvatarURL({
dynamic: true,
format: 'png',
}),
);
embed.setDescription(`${user} has react a: ${reaction.emoji}`);
channel.send(embed);
}
}
Visual Representation
New
in the code if (reaction.message.id === '730000158745559122') the 73000 portion represents that whole message as a focal point on what the bot is 'listening' for. Any reaction to it, be it 1, 2, or 3 will trigger this singular process. So by what you are saying by using the (id === 123 || id === 456 || id === 789) {} would likely be the way to go if I had more than one message is was 'collecting' on for a lack of better way to state it. I believe what I need to do is
`if (reaction(':one:').message.id === 123 {send message ('nifty embed')to channel.id 12345}
if (reaction(':two:').message.id === 123 {send message ('nifty embed') to channel.id 67890}
if (reaction(':three:').message.id === 123 {send message ('nifty embed') to channel.id 16273}`

Discord.py: `on_member_update()` called repeatedly [1 guild only]

I want my bot to change user roles depending on nickname patterns (basically the company ID).
When the user puts his ID in the nickname, on_member_update() is called normally.
In the function, the bot adds roles and changes the nickname again to a specific pattern. This triggers the on_member_update() yet again.
Note that I have the bot in 1 guild only.
I tried to stop it by adding
if before.display_name == after.display_name:
return
But it still enters the function when the nickname is changed. Is there a way to avoid the function triggering itself again?
The code:
#bot.event
async def on_member_update(before, after):
if before.display_name == after.display_name:
return
id = re.findall(r'\d{6,7}', after.display_name)
if not id:
return
else:
# Business logic (changing nickname, adding roles etc...)
This is not an entire answer but it will guide you in the right direction.
You have to keep record of ids of changed users either in a database or using a JSON file. I called it users_changed which should be a list.
Note: that id is reserved in Python you must use another thing maybe even id_
users_changed = [11111,22222,33333] # get this from the db or file.
#bot.event
async def on_member_update(before, after):
if before.id in users_changed:
return
# code here
# then add before.id into the users_changed

Alexa skill response pagination

In one intent of my skill have lot of records to display or read for user; i want to paginate response of that intent
Example:
User: how many announcements are in the system
Alexa: there are 6. first 4 are (announcement 1, announcement 2, announcement 3, announcement 4)
Do you want to hear more?
User: Yes
Alexa: Next 2 announcements are (announcement 5, announcement 6)
Use AMAZON.YesIntent intent to capture "Yes" inputs from the user.
When the user asks for announcements use sessionAttributes along with your response to keep track of the read announcement indexes. So that when the user says "Yes", you can use this session attribute to read the next set of announcements. You can also set a STATE attribute too, so that you can validate the state in AMAZON.YesIntent handler before you give the next set of announcements.
Ex:
...
"sessionAttributes": {
"announcements_index": [0,1,2,3],
"STATE": "READ_ANNOUNCEMENTS"
}
...
When the user say "Yes", in your AMAZON.YesIntent handler check whether the state is READ_ANNOUNCEMENTS and based on the announcements_index give the next set of announcements from your announcements list. And in sessionAttributes update the announcements_index.
There is chance that user might say "No", for "Do you want to hear more?". So add a AMAZON.NoIntent too and handle it accordingly.
Do not forget to clear announcements_index and STATE when use case is done.
More on sessionAttributes and Response Parameters here

Resources