How to stop discord python bot from repeating a response?

Viewed 181

I am a big beginner so I apologise if this seems like a simple question. I am trying to use the Discord's Python discord.py and make the bot respond to a message it gets, and then have the user respond to the message & the bot respond again etc. But I notice if the user types the initial message multiple times, the bot will respond to the second message multiple times. For example:

User: !newuser

Bot: Hello!

User: !newuser

Bot: Hello!

User: hello

Bot: Hello user!

Bot: Hello user!

I want to make it so that, even if the user types the first command multiple times, the bot will only respond to the latest one (and ignore all the extra/pointless commands). This is what I did so far:

@client.event
async def on_message(message):
    if message.author == client.user:
        return
    if message.content.startswith('!newuser'):
        message.author == username
        channel = message.channel
        await channel.send('Hello!')

        def check(m):
            return m.content == 'hello' and m.channel == channel

        msg = await client.wait_for('message', check=check)
        await channel.send('Hello {.author}!'.format(msg))
2 Answers

I am not very acquainted with discord.py, but

You are using an "async" function, which behaves differently than a normal function. Basically, while runing the main event, python will create a coroutine object everytime the function is called upon, allowing for multiple objects to be created and, consequently, those objects will all respond later on when you call them back.

Maybe you should try sync functions instead, that way you could prevent that the function will be called again when one object has been created already.

If that doesn't work for you (because, let's say, you still want to listen to many users at once), then another solution would be to remember the people you have already listened to. This can be done very roughly by storing the author of the message on a list and checking before running the function if that person has been listed already:

@client.event
list_of_people=[]
async def on_message(message):
    message.author = username
    if message.author == client.user:
        return
    if message.content.startswith('!newuser') and username not in list_of_people:
        list_of_people.append(username)
        channel = message.channel
        await channel.send('Hello!')

        def check(m):
            return m.content == 'hello' and m.channel == channel

        msg = await client.wait_for('message', check=check)
        await channel.send('Hello {.author}!'.format(msg))

If the intention here is to temporarily timeout the command's use to prevent multiple outputs from being generated together, then the below approach is a possible solution.

You can define a variable with the amount of seconds you want to "timeout"(COMMAND_TIMEOUT), the command and/or it's output, and then compare the creation time of the current and previous messages to check whether the time passed is greater than or equal to the COMMAND_TIMEOUT.

To find the previous message of the channel we get the history of the channel(limit = 1 and before the creation time of the current message), using the discord.TextChannel.history() method, and .flatten() it as a list then fetch the zeroth(0th) index of it as the previous message before the current message that triggered the command.

NOTE: For the correct code look at the edit section, the code below has certain errors, but for understanding sake, this section is still intact.

Like so -:
COMMAND_TIMEOUT = 5 # Number of seconds as timeout to ignore any extra calls to the same command.
prev_message = await channel.history(limit = 1, before = m.created_at).flatten()[0]    
return m.content == 'hello' and m.channel == channel and (m.channel.created_at - prev_message.created_at).total_seconds() >= COMMAND_TIMEOUT

The full code then becomes -:

@client.event
async def on_message(message):
    if message.author == client.user:
        return
    if message.content.startswith('!newuser'):
        
        message.author == username
        channel = message.channel
        await channel.send('Hello!')

        def check(m):
            COMMAND_TIMEOUT = 5 # Number of seconds as timeout to ignore any extra calls to the same command.
            prev_message = await channel.history(limit = 1, before = m.created_at).flatten()[0]    
            return m.content == 'hello' and m.channel == channel and (m.channel.created_at - prev_message.created_at).total_seconds() >= COMMAND_TIMEOUT
        
        msg = await client.wait_for('message', check=check)
        await channel.send('Hello {.author}!'.format(msg))

NOTE:


EDIT: As mentioned by @Chuaat, my previously shown approach was not fully correct, as the history function is an async function and cannot be called within check as it is a sync function.

The fix to this can be to simply replace, the call of the history function from inside to outside the check function. This can be done using a lambda statement, the changes will be as follows -:

CHANGES TO THE CHECK FUNCTION: Remove the call of the history function from within, and rather add an extra argument in the check function for the prev_messagelike so -:

def check(m, prev_message) :
            COMMAND_TIMEOUT = 5 # Number of seconds as timeout to ignore any extra calls to the same command.
            return m.content == 'hello' and m.channel == channel and (m.channel.created_at - prev_message.created_at).total_seconds() >= COMMAND_TIMEOUT

CHANGES TO THE WAIT_FOR FUNCTION'S PASSED ARGUMENTS: Now the check function passed to the wait_for function has to be modified to a lambda statement like so -:

msg = await client.wait_for('message', check = lambda m : check(m, await channel.history(limit = 1, before = m.created_at).flatten()[0]))

The corrected full code now becomes -:

@client.event
async def on_message(message):
    if message.author == client.user:
        return
    if message.content.startswith('!newuser'):
        
        message.author == username
        channel = message.channel
        await channel.send('Hello!')
        
        def check(m, prev_message) :
            COMMAND_TIMEOUT = 5 # Number of seconds as timeout to ignore any extra calls to the same command.
            return m.content == 'hello' and m.channel == channel and (m.channel.created_at - prev_message.created_at).total_seconds() >= COMMAND_TIMEOUT
        
        msg = await client.wait_for('message', check = lambda m : check(m, await channel.history(limit = 1, before = m.created_at).flatten()[0]))
        await channel.send('Hello {.author}!'.format(msg))
Related