How to add a function to discord.py event loop? - python

I am using Python with discord.py. Documentation here
I've got a bot that is running on a Discord server that links the server with a subreddit. Users have various commands that do things like getting the top submissions, getting the latest submissions, and so on.
I want to add some features to the bot, with one of them being a keyword notifier. The bot should search the subreddit for keywords in the title, and then notify users if they are on the list for that keyword. I know how to do this, I've done it plenty of times, but I don't know how to do it with a Discord bot. I have no experience with asynchio or any kind of asynchronous programming.
The way I've tried to do it works, but it is very janky and definitely not good. At the top of the on message() function, I just add a call to the search_submissions() function, so that whenever someone puts sends a new message on the server, the bot will scan the Reddit submissions. The server is busy enough that this would work relatively okay, but I really want to do it the "proper" way.
I don't know how to call the search_submissions() function without putting it inside of on_message().
Edit for extra code:
import discord
TOKEN = "redacted"
client = discord.Client()
#client.event
async def reddit_search():
print("Searching")
#client.event
async def on_message(message):
if message.content.startswith("reddit!hot"):
# Get hot
# Do other things.
#client.event
async def on_ready():
print("Connected to Discord as {}.".format(client.user.name))
client.run(TOKEN)

You can add a function to the bot event loop with Client.loop.create_task(search_submissions()) like this:
async def search_submissions():
pass
client = discord.Client()
client.loop.create_task(search_submissions())
client.run(TOKEN)
Update:
If you want your function to continue working you can put it in a while loop with some sleeping in between:
async def search_submissions():
while(true):
# do your stuff
await asyncio.sleep(1)

The other answers here don't take into account discord.py's helpful tasks.loop decorator.
To make an event occur every 5 seconds, you would use
from discord.ext import tasks, commands
class MyCog(commands.Cog):
def __init__(self):
self.foo.start()
def cog_unload(self):
self.printer.cancel()
#tasks.loop(seconds=5.0)
async def foo(self):
print('bar')
More can be found here: https://discordpy.readthedocs.io/en/latest/ext/tasks/

You want your search_submissions() function to be async so other functions of your bot can still be invoked and your bot stays responsive. Define it to be def async and use aiohttp to send async HTTP requests to reddit -- what this does is send off the request, relinquish control to the event loop, and then take back control once the results have been transmitted back. If you use a standard HTTP library here instead then your whole bot will be blocked until the result comes back. This of course only makes sense if the task is mainly I/O-bound and less CPU-bound.
Then call search_submissions() in on_message(message) -- but call it asynchronously using result = await search_submissions(). This will resume execution of on_message once the result of search_submissions is ready.
If you truly want to do something else in the same context while waiting on search_submissions (which I think is unlikely), dispatch it as task = asyncio.create_task(search_submissions()). This will start the task immediately and allow you to do something else within the same function. Once you need the result you will have to result = await task.
async def search_submissions():
async with aiohttp.ClientSession() as session:
async with session.get(some_reddit_url) as response:
return await response.read()
#client.event
async def on_message(message):
if message.content.startswith("reddit!hot"):
result = await search_submissions()
await message.channel.send(result)

Related

Single action script for discord bot with python discord.py

I understand that usually the discord bots are in a listening (blocking) loop, but how can I create a function that connects, send a message or perform any action and disconnect in a non blocking flow?
I'm using discord.py and I'm looking for something like:
import discord
TOKEN = "mYtOkEn"
discord.connect(TOKEN)
discord.send("I'm sending this message")
discord.disconnect()
I already tryied playing with the async but have problems with the threading, so was wondering if there is something more simple.
It is for a button that when clicked, perform that action but after that it can continue working on other tasks
Thanks beforehand
One way you could accomplish this is by using a custom event loop.
Example:
import discord
import asyncio
from threading import Thread
TOKEN = "secret"
client = discord.Client()
def init():
loop = asyncio.get_event_loop()
loop.create_task(client.start(TOKEN))
Thread(target=loop.run_forever).start()
#client.event
async def on_message(message):
if message.author == client.user:
return
await message.channel.send('Hello!')
#client.event
async def on_ready():
print("Discord bot logged in as: %s, %s" % (client.user.name, client.user.id))
init()
print("Non-blocking")
Take a look at this for more info: C-Python asyncio: running discord.py in a thread
Thank you for your help and support. With the SleepyStew answer I could find the path to solve it and went this way:
import discord
import asyncio
def discord_single_task():
# Define Coroutine
async def coroutine_to_run():
TOKEN = "Secret"
# Instantiate the Client Class
client = discord.Client()
# # Start (We won't use connect because we don't want to open a websocket, it will start a blocking loop and it is what we are avoiding)
await client.login(TOKEN)
# Do what you have to do
print("We are doing what we want to do")
# Close
await client.close()
# Create Loop to run coroutine
loop = asyncio.new_event_loop()
llll = loop.create_task(coroutine_to_run())
loop.run_until_complete(llll)
return 'Action performed successfully without a blocking loop!'

Discord.py Bot Takes too Long to Respond

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.
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:
discord.Client.loop
loop.create_task
Task.cancel
asyncio.sleep
discord.Client.is_closed
Replit db

How to make a Discord Bot that can accept arguments from Python of what message to send to who and be able to close itself?

I'm trying to do something pretty simple. I just want Python to be able to call a quick function that sends a direct message over Discord. Basically a modified version of the FAQ example. I want another Python class to start the Discord client and pass the message string and user id. And I want the Discord client to simply send the string and then close and delete the entire class. After looking through docs I made these modifications:
import discord
class MyClient(discord.Client):
async def on_ready(self):
print('Logged on as {0}!'.format(self.user))
user = await self.fetch_user(self.destination_user)
await user.send(self.message)
await self.close()
def __init__(self, user_id, user_message):
self.destination_user = user_id
self.message = user_message
client = MyClient(desired_id, desired_message)
client.run('My Client ID')
#wait for client to end and then continue
However, I'm running into 2 problems. It looks like discord.Client doesn't allow an __init__() function, and when I try to do async def __init__() there's also an error. Are there other ways to pass arguments to it through Python, rather than react to messages in Discord? Also, self.close() results in "RuntimeError: Cannot close a running event loop". What's the proper way to wait for on_ready() to finish and then close it?
I can get it working by hardcoding the user ID and message, eliminating __init__() and simply not closing the class. However, for some reason, even then self.get_user() doesn't work, only self.fetch_user(). The docs say to use get_user(), so I'd prefer that, but I don't know what I'm doing wrong. My bot is in the same server as the target user, and I've given it both privileged intents, but get_user() continues to return "None", whereas fetch_user() properly messages the user, with the same exact arguments.
I found out what seems the proper way to run discord.Client in this fashion:
import discord
import nest_asyncio
import asyncio
nest_asyncio.apply()
class DiscordClient(discord.Client):
async def on_ready(self):
user = await self.fetch_user(self.send_user)
await user.send(self.message)
async def on_message(self, message):
#Pause so there's no warning about operations not finishing when class closes
await asyncio.sleep(0.1)
await DiscordClient.close(self)
async def parameters(self, user, message):
self.send_user = user
self.message = message
async def send_discord(user, message):
print("Sending Discord message")
client = DiscordClient()
await client.parameters(user, message)
await client.start('Private Key')
print("Discord message sent")
asyncio.run(send_discord(user_id, "Test message contents"))
on_message runs even it's the bot that sent the message, so it will quickly close itself. I had to add the wait or there were some race conditions or something causing a warning every time it closed.

Discord.py-Rewrite Cycle through statuses using background tasks

My Goal
Hello, I want my bot to cycle through 3 prefixes, The prefix for the bot, The amount of servers its in, and another message of my choosing. I use the #bot.command/#bot thing, so the example in the FaQ in the readthedocs website doesn't help me. What i tried didn't work, and i also do not know how to get the number of servers the bot is in.
What I tried
async def my_background_task(self):
await bot.change_presence(activity=discord.Game(name="message"))
await asyncio.sleep(7)
await bot.change_presence(activity=discord.Game(name="PREFIX: CF>"))
await asyncio.sleep(7)
#bot.event
async def on_ready(self):
print('Bot is online and ready.')
self.bg_task = self.loop.create_task(self.my_background_task())
This solution has been tested:
prefixes = [lambda: 'prefix1', lambda: 'prefix2', lambda: str(len(bot.guilds))]
bot = commands.Bot(command_prefix=prefixes[0]())
async def my_background_task():
prefix_num = 0
while True:
prefix = prefixes[prefix_num]()
await bot.change_presence(activity=discord.Game(name=prefix))
bot.command_prefix = prefix
# increase the current prefix - if it's reached the length of the prefix list, set it back to 0
prefix_num = (prefix_num + 1) % len(prefixes)
await asyncio.sleep(7)
#bot.event
async def on_ready():
bot.loop.create_task(my_background_task())
First of all, your background task and on_ready function shouldn't have any self in them. Also, your background task has to have a while loop inside of it, otherwise, it will only run once.
This code utilizes anonymous lambda functions because it allows the bot to be added to a different server and adjust the prefix when that happens. When it comes time to change the prefix, one of the functions is called, returning a string.
I would also recommend looking at the API reference because it is very helpful.

Discord.py: Why do the welcome and goodbye messages repeat?

Problem: I am trying to have it so that the bot sends a single message in general, but the messages continuously repeat, seemingly increasing each time this occurs. Also, if I switch the message to something else, the repeated parts are of the old message.
Code:
import discord
from discord.ext import commands
client=commands.Bot(command_prefix = '.')
#client.event
async def on_ready():
print('ok')
#client.event
async def on_member_join(member):
channel=discord.utils.get(member.guild.channels, name="general")
await channel.send("Hi {}".format(member))
#client.event
async def on_member_remove(member):
channel=discord.utils.get(member.guild.channels, name="general")
await channel.send("Bye")
client.run(token)
Since the code is under the function variable then you have to end each function.
It might be due to multiple instances of the application are running. Check your task manager and search for python process, End it if there are multiple and re-run the script.

Categories