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? - python

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.

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!'

How to ignore messages until after on_ready finishes in discord.py

I am writing a discord bot using discord.py, and I ran into some problems. I have a rather large database that needs initialising every time the bot starts to register the new servers/players. And that process takes a long time (20-30s). I am initialising the database inside the on_ready method, so I can have access to all the guilds. And as a result, all the commands that users used during that period are queued and my bot will then "burst" out all the responses for each of the commands. I want to be able to completly ignore messages sent during that initialisation process, to prevent possible rate limits on certain APIs.
I have tried searching on Google/StackOverflow and looking through discord.py docs, but sadly, no results.
here is my code:
async def on_ready(self):
logger.info("INITIALISING DATABASE...")
self.init_guilds()
# initialising other data, takes about 20-30s
logger.info("CLIENT LOGIN")
async def on_message(self, message);
if message.author.bot or not isinstance(message.channel, discord.TextChannel):
return
msg = message.content.strip()
if len(msg) > 1 and msg[0] == '_':
await handler.handle(message, self) # I am using a custom command handler instead of discord.ext.commands
async def on_connect(self):
print("Initialising database...")
self.init_guilds()
async def on_ready(self):
print(f"{self.user} is ready!")
async def on_message(self, message):
if not self.is_ready():
return
You can have a flag which indicates if the database is initialized (set it to False by default), after you connect to the database set it to True. Inside the error handler check whether it's set to True:
def __init__(self, ...):
self.db_connected = False
...
async def on_ready(self):
logger.info("INITIALISING DATABASE...")
self.init_guilds()
# initialising other data, takes about 20-30s
logger.info("CLIENT LOGIN")
self.db_connected = True
async def on_message(self, message):
if not self.db_connected:
return
if message.author.bot or not isinstance(message.channel, discord.TextChannel):
return
msg = message.content.strip()
if len(msg) > 1 and msg[0] == '_':
await handler.handle(message, self)
PS: Connecting to your database and/or making API requests in the on_ready event is a bad idea, from the docs:
This function is not guaranteed to be the first event called. Likewise, this function is not guaranteed to only be called once. This library implements reconnection logic and thus will end up calling this event whenever a RESUME request fails.
Unless you implement some kind of logic to prevent the event to be called more than once (like another flag or a decorator), I wouldn't recommend you connecting to the database there.

Why am I getting the error "The command is already an existing command or alias" when it shouldn't be?

I'm just trying out with making Discord bots and I tried putting this command in a category, however, this error shows up no matter what I call the command.
Here's my code:
import discord,random
from discord.ext import commands
bot = commands.Bot(command_prefix=';')
#bot.event
async def on_ready():
print("bot is ready for stuff")
await bot.change_presence(activity=discord.Game(name=";help"))
class general_stuff(commands.Cog):
"""Stuff that's not important to the bot per say"""
#bot.command()
async def lkibashfjiabfiapbfaipb(self, message):
await message.send("test received.")
bot.add_cog(general_stuff())
bot.run("TOKEN")
and this is the error I get back:
The command lkibashfjiabfiapbfaipb is already an existing command or alias.
No matter how much I change the command, it keeps giving the same error.
You are on the right track. The reason you are getting the error is when you start the program, it reads from the top and works its way down;
class general_stuff(commands.Cog):
"""Stuff that's not important to the bot per se"""
#bot.command() # Command is already registered here
async def lkibashfjiabfiapbf(self, message):
await message.send("test received.")
bot.add_cog(general_stuff())
# It tries to load the class general_stuff, but gets the error because
# it's trying to load the same command as it loaded before
#bot.command adds the method to the bot and loads it. With Cogs, you operate with #commands.command(). It only converts the method to a command, but does not load it in.
Your code should look like
...
class general_stuff(commands.Cog):
"""Stuff that's not important to the bot per se"""
#commands.command()
async def lkibashfjiabfiapbf(self, message):
await message.send("test received.")
...
References:
#bot.command
#commands.command

How to toggle trigger Cog listener with command in discord

I am currently trying to make a discord bot where when you type the command !purge in the desired channel, it will constantly delete all the messages sent, and when you type it again, it will stop deleting all the messages.
From learning online I know I have to use a Cog listener, but I don't exactly know how to "trigger" it with a command. I would need the #commands.Cog.listener() paragraph to use the on_message listener, but I also can't figure out how I would get it to delete messages in the channel where the command !purge was executed.
I tried already using a boolean value to toggle on and off when the command was typed and it would use a while loop to constantly delete messages, but then the while loop would stop. It might have been because the session of the command expired, not sure about that though.
Any thoughts on how I can use get this working? More specifically, how I can somehow link the Cog to the command? Thanks!
(Edits of my code I tried to use to be here but I have deleted them because of irrelevancy)
Cog Thread I found: https://stackoverflow.com/a/53528504/11805086
You'd have to create a trigger command, which enables or disables the purge mode and then, in your on_message function, you'd have to check if the purge mode is enabled or not.
Here's how to do it (within a cog):
Edited by poster
In cogs.py
from discord.ext import commands
class Purge_Cog(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.channels = {}
#commands.command()
async def purge(self, ctx):
try:
if self.channels[ctx.channel]:
self.channels[ctx.channel] = False
await ctx.channel.send("Purge mode disabled!")
else:
self.channels[ctx.channel] = True
await ctx.channel.send("Purge mode enabled!")
except:
self.channels[ctx.channel] = True
await ctx.channel.send("Purge mode enabled!")
#commands.Cog.listener()
async def on_message(self, message):
try:
if self.channels[message.channel]:
await message.channel.purge(limit=999)
except:
pass
def setup(bot):
bot.add_cog(Purge_Cog(bot))
In your main file add this (bot.py or main.py)
import discord
from discord.ext import commands
initial_extensions = ['cogs.cogs']
if __name__ == '__main__':
for extension in initial_extensions:
bot.load_extension(extension)
bot.run('Your token')

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

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)

Categories