How to Make a Looping Discord Bot Task that is Invoked When a Message is Posted in Python

Question:

I am trying to write a discord bot that posts yeterday’s Wordle solutions but I cannot seem to figure out how to get a task to be invoked by a message and then have that task loop. I tried to use a while loop but then the bot would only work for one server. Trying to use a looping task does not work either. Here is the code

import imp
import json
import requests
import discord
import os
import time
from datetime import date, timedelta
from discord.ext import tasks


client = discord.Client()
#channel_id_exists = False
#channel_id = 0


@client.event
async def on_ready():
    print('We have logged in as {0.user}'.format(client))


@client.event
async def on_message(message):
    if message.author == client.user:
        return

    if message.content.startswith('!wordle_setup'):
        await message.delete()
        await message.channel.send("Setting up...")
        channel_id = message.channel.id
        await wordle_guess(channel_id).start()


@tasks.loop(seconds=10)
async def wordle_guess(channel_id):

    message_channel = client.get_channel(channel_id)
    yesterday = date.today() - timedelta(days=1)

    year = yesterday.year
    month = yesterday.month
    day = yesterday.day

    word_guess = json.loads(requests.get(
        "https://najemi.cz/wordle_answers/api/?day={0}&month={1}&year={2}".format(day, month, year)).text)["word"]
    await message_channel.send("Word guess for " + yesterday.isoformat() + " : " + word_guess + 'nhttps://www.merriam-webster.com/dictionary/' + word_guess)

client.run("TOKEN")

Either the bot gets stuck at setup, or the the task doesn’t loop in 10 seconds, or the bot only works for one server. I use the setup command to prevent needing to hardcode the channel id into the code.

Asked By: Karma Cool

||

Answers:

I have solved the issue by wrapping the code in a client class and allowing the storage of multiple channel ids. The new code is shown here.

import asyncio
import imp
import json
import requests
import discord
import os
import time
from datetime import date, timedelta
from discord.ext import tasks
import threading

print("test!")


class MyClient(discord.Client):
    channel_id = []
    channel_id_exists = False

    async def on_ready(self):
        print('We have logged in as {0.user}'.format(self))

    async def on_message(self, message):
        if message.author == self.user:
            return

        if message.content.startswith('!wordle_setup'):
            await message.delete()
            await message.channel.send("Setting up...")
            self.channel_id.append(message.channel.id)
            self.channel_id_exists = True

    @tasks.loop(seconds=3600*24)
    async def wordle_guess(self):
        if self.channel_id_exists:

            yesterday = date.today() - timedelta(days=1)

            year = yesterday.year
            month = yesterday.month
            day = yesterday.day

            for channel_id_iter in self.channel_id:
                message_channel = self.get_channel(channel_id_iter)
                word_guess = json.loads(requests.get(
                    "https://najemi.cz/wordle_answers/api/?day={0}&month={1}&year={2}".format(day, month, year)).text)["word"]
                await message_channel.send("Wordle guess for " + yesterday.isoformat() + " : " + word_guess + 'nhttps://www.merriam-webster.com/dictionary/' + word_guess)


client = MyClient()
client.wordle_guess.start()
client.run("TOKEN")

Answered By: Karma Cool

You can also launch a task from a command.
In my case, using the module interactions, I have a discord function that has to run every hour or when the command is triggered. There may be better solutions but this one worked for me.

from discord.ext import tasks
import interactions
...
bot = interactions.Client(bot_token,...)
...

# manually triggered command
@bot.command( name='sync_roles', ...)
async def command_sync_roles(ctx: interactions.CommandContext):
    global busy
    if busy: return
    bot._loop.create_task(sync_roles())

# scheduled job
@tasks.loop(hours=1)
async def cron_sync_roles():
    global busy
    if busy: return
    bot._loop.create_task(sync_roles())

# I use busy as a global variable to avoid double calls
busy = False
async def sync_roles():
    global busy
    busy = True
    ## Your code here
    busy = False

...
cron_sync_roles.start()
bot.start()
Answered By: Nacho R