0

Goal:

I'm developing a discord bot which scans a url every 5 seconds or so, checks for a specified change on that webpage, and will send a message in the discord channel if that change occurs. I've done this by sending the url to the bot using an if statement in on_message. The url is then passed to a tasks.loop() function, where it is scanned and processed in another function for the change.

Problem:

I'd like to be able to send a message in the discord channel which quickly ends the process taking place in the tasks.loop(), so that I can pass it a different url to scan using the on_message function. In its current form, it works-- just very slowly. From the time the cancel trigger is sent, it takes around 3 minutes to send the verification message that the process has been cancelled. I need to make this 5 seconds or less. For what its worth, the bot is kept running using replit and uptime robot, but I am sure that the long response time is not related to the frequency the repl is awoken by uptime robot.

Code:

My code is much more complex and riddled with obscurely named variables, so here is a much simpler snippet of code with the same general structure.

client = discord.Client()
channel = client.get_channel(CHANNEL_ID)

@tasks.loop()
async def myloop(website, dataframe):
    
    channel = client.get_channel(CHANNEL_ID)
    
    try:
        # iteratively scrape data from a website for
        # a predefined change in the dataframe
        if change = True:
            await channel.send(notification)
            
    except:
        pass


@client.event
async def on_message(message):
    
    channel = client.get_channel(CHANNEL_ID)
    msg = message.content
    
    if msg.startswith('track'):
        
        website = msg[6:]
        await channel.send('Now tracking '+str(website))
        myloop(website,df)

    if msg.starswith('stop'):
        
        myloop.cancel()
        await channel.send('Done tracking, awaiting orders.')

        

Attempted Solutions:

I have tried using some forms of threading, which I am very new to, and I haven't found a way to make it work any faster. Any suggestions or solutions would be greatly appreciated! I've been combing the web for help for quite some time now.

Seth Bowers
  • 31
  • 1
  • 6

1 Answers1

1

Looks like you could use client.loop.create_task to create asyncio task objects, and their cancel method to immediately cancel those asyncio tasks at the right time, e.g.

import asyncio
from replit import db


_task = None


async def myloop():
    website = db['website']
    dataframe = db['dataframe']
    channel = client.get_channel(CHANNEL_ID)

    while not client.is_closed():
        await asyncio.sleep(5)
        try:
            # iteratively scrape data from a website for
            # a predefined change in the dataframe
            if change:
                await channel.send(notification)
        except:
            pass


@client.event
async def on_message(message):
    global _task  # This gives the function access to the variable that was already created above.
    msg = message.content
    
    if msg.startswith('track'):
        website = msg[6:]
        await message.channel.send('Now tracking '+str(website))
        db['website'] = website
        db['dataframe'] = df
        if _task is not None:
            _task.cancel()
        _task = client.loop.create_task(myloop())

    if msg.startswith('stop'):
        if _task is not None:
            _task.cancel()
            _task = None
            await message.channel.send('Done tracking, awaiting orders.')

The argument create_task takes is a coroutine that takes no arguments, so the website URL and dataframe need to be accessible to the function a different way (I'm not sure which way you would prefer or would be best; using replit's db is just an example).

With this approach, you should be able to use track again to change which website is being monitored without using stop in between.

More details in the docs:

wheelercj
  • 127
  • 5
  • This seems to be working, but it still had an error. The _task variable was not within the scope of the on_message event, which I fixed just by adding ```db['Task'] = None``` and replacing each _task instance with the same reference of db['Task']. It does seem to be working, but I get a long console error on the line ```client.loop.create_task(myloop())``` which ultimately returns: ```ValueError: Circular reference detected```. Have I mucked up with my solution to the first _task hot-fix? – Seth Bowers Aug 13 '21 at 16:34
  • I fixed two mistakes in my answer above. I tried running the code and got the same error. It can be fixed by changing `db['Task']` back to `_task` and putting `global _task` near the top of `on_message` to give the function access to the variable. The other mistake was misspelling `startswith` as `starswith`. If you continue needing more global variables, you may want to consider subclassing `discord.Client` or `discord.Bot` so you can turn the global variables into class attributes and be less likely to get more errors in the future. – wheelercj Aug 14 '21 at 01:33
  • 1
    Oh! I see now. Thanks @wheelercj ! Happy coding – Seth Bowers Aug 16 '21 at 20:15