How can I run a command from another command? - python

I'm trying to write a Discord bot, where if I run a certain command, a part of the program will run, but the other part won't.
For example: In my case, when they type in "?m", they will get a message saying "xy", but then for 90 seconds, they won't be able to call that command again. If they do call it in this 90 second period, another command called "?x" will run.
Can someone help me out?

You can do it using bot.invoke() method:
#bot.command()
async def x(ctx):
pass # whatever here
#bot.command()
async def m(ctx):
if something:
ctx.command = bot.get_command("x")
await bot.invoke(ctx) # invoke a command called x

With Python's time module, you can check how long it's been since the command was last successfully called. time.time() returns the current time (expressed as a number of seconds since January 1st, 1970).
Calling one command from within another can be as simple as calling it with await, like any other coroutine, and passing the same context to it.
import time
from discord.ext.commands import Bot
from tokens import TOKEN
bot = Bot(command_prefix='?')
# Set the initial time as 0 (effectly 1970) so it was definitely long
# ago for the command to now be called.
last_called = 0
#bot.command()
async def hi(ctx):
# Compare the current time to when the command was last successfully
# called.
global last_called
now = time.time()
if now - last_called <= 90:
await not_now(ctx)
else:
last_called = now
await ctx.send('Hello')
#bot.command()
async def not_now(ctx):
await ctx.send('Please wait a minute.')
bot.run(TOKEN)
Using global variables is not generally the best idea, but unfortunately subclassing Bot for tidier encapsulation of data into a single object is quite clunky.

Related

rotating status and command usage

so, im trying to make a multiple status for my discord bot, but i cant find anything.
also im trying to make the status show how many times two commands have been used globally
this is what ive tried for multiple status
async def change_playing():
threading.Timer(10, change_playing).start()
await test_bot.change_presence(game=discord.Game(name='Currently on ' + str(len(client.servers)) +
' servers'))
threading.Timer(10, change_playing).start()
await test_bot.change_presence(game=discord.Game(name='Say shelp'))```
i found it, but thanks for the help
guilds = len(client.guilds)
users = len(client.users)
await client.wait_until_ready()
statuses = ["Shelp", f"im in {guilds} guilds| watching {users} users", "with knives"]
while True:
game = discord.Game(random.choice(statuses))
await client.change_presence(status=discord.Status, activity=game)
await asyncio.sleep(10)
client.loop.create_task(roll_presence())```
You could add this to your bot with the discord.ext.tasks decorator that was made for the very purpose of repeating an action every n seconds.
The docs for it are here.
You could do something like
import discord
from discord.ext import tasks
SAYHELP = discord.Game(name="Say shelp")
SERVER_COUNT = discord.Game(name=f'Currently on {len(client.servers)} servers'))
#tasks.loop(seconds=10)
async def change_playing():
if test_bot.activity is SAYHELP:
test_bot.change_presence(game=SERVER_COUNT)
else
test_bot.change_presence(game=SAYHELP)
async def on_ready(self, *args, **kwargs):
change_playing.start()
super().__on_ready__(*args, **kwargs)
Just don't forget to update the SERVER_COUNT activity whenever the bot gets added to another server. There's this event that gets fired then.
As for the command usage counter, there's a couple approaches to this that depend entirely on how you're instantiating your bot.
If you're just using the discord.ext.commands.Bot class as-is, you'll want to define an integer for that in your top-level code and increment that every time the relevant command is called.
If you're subclassing the bot, however, then you can define self.commandcounter in your bot and have the commands update that instead.
If you're also using cogs, then make sure the cog has a reference back to the bot so you can increment self.bot.commandcounter.
You could then have the #task decorated function set that as presence as well. Might need to add another check for the current activity though.
The docs for dpy are a good place to start looking if you can't get something to work.

(Python) How to make discord bot respond able to respond to multiple users at the same time

I created a discord bot in python that runs some other code if a user sends it a message. The problem I'm having is that while the code is being run, the bot will not respond to any other messages. How can I make it so that while the bot is running code behind the scenes it can still respond to other messages?
Code looks like this:
from secrets import TOKEN
import discord
def code_that_takes_time():
items = []
for x in range(10):
item = do_something() #get request in my case. takes 20s per iteration
items.append(item)
return items
#client.event
async def on_message(message):
user_message = str(message.content)
send = message.channel.send
if message.author == client.user:
return
if user_message.startswith('$do'):
result = code_that_takes_time()
send(result)
if user_message.startswith('hello'):
send('hello, nice to meet you')
client.run(TOKEN)
I want it so that the bot can still respond to the 'hello' message while it's running code_that_takes_time and then send the result whenever it finishes.
Thanks in advance for the help and ideas.
anything that has a chance of blocking, such as your code_that_takes_time should ideally be written as a co-routine so that it will sleep and let other code execute while it is waiting. I'm relatively new to asyncio myself, but I believe the syntax is to use async on your code_that_takes_time, i.e.:
async def code_that_takes_time:
items = []
for x in range(10):
item = do_something() #get request in my case. takes 20s per iteration
items.append(item)
return items
then make sure to call it with the await keyword (if you try to call a coroutine normally, the call just retrieves and returns the coroutine object), e.g.:
#put any needed arguments in the parentheses as with a normal call
await code_that_takes_time()
I wish I could be more help than that, but I'm also a newb with this stuff and don't want to mislead.

How To Loop Tasks In Discord.Py With Different Loops For Each Guild

I want to loop a task in Discord.py that will loop on several different guilds at different times. For example, if one guild calls this command at 20:00 and another guild calls the command at 13:00, I want to be able to run the command each day at the same time for each guild (at the time they specified). I also want those commands to reload when the server is restarted after maintenance.
So, basically, I want a certain command to run every day at different times for each server. I also want the timers to save so that they are there after a server restart.
The way I am doing this right now is by having an external database that stores all the necessary information required to run the command, loading that information on start up, and calling the async command on start up, basically reloading all the saved timers.
Here is the code I have thus far.
#commands.command(aliases=['dp'])
#commands.has_permissions(administrator=True)
async def dailyprompt(self, ctx, arbitrary_prefix, channel: discord.TextChannel, time_to_run: str, prompt=True): # This command seriously needs a database to run more efficiently. For now though, I just want it working.
if arbitrary_prefix.lower() == 'set' and channel is not None and time_to_run is not None:
time_zone = pytz.timezone('EST')
now = datetime.now(time_zone)
datetime_to_run = f'{str(now.date())} {time_to_run}'
datetime_to_run = datetime.strptime(datetime_to_run, '%Y-%m-%d %H:%M').astimezone(time_zone)
difference = (datetime_to_run - now).total_seconds()
if difference < 0:
datetime_to_run += timedelta(hours=24)
difference = (datetime_to_run - now).total_seconds()
if difference < 0:
await ctx.channel.send('Please input a valid time.')
raise ValueError
embed = discord.Embed(title='Daily Prompt Admin Notification', description=response, color=self.color)
await ctx.author.send(embed=embed)
await asyncio.sleep(difference)
await self.prompt(channel)
await asyncio.sleep(1)
await self.dailyprompt(self, ctx, arbitrary_prefix, channel, time_to_run)
You should store data into a .json file, like this:
from json import load, dumps
global Path
Path = r"C:\\Path\to\File.json"
global Dictionary
Dictionary = {}
...
#Bot.command()
async def SetHour(ctx, hour:int):
#Put there functions to make hour to be a datetime.data or datetime.time object
global Dictionary
Dictionary[ctx.guild.id] = hour
DictionaryEncoded = dumps(Dictionary)
Database = open(Path, 'w')
Database.write(DictionaryEncoded)
Database.close()
Did you need smth like this?

Pinging someone at an interval discord.py

I am making a bot and want it to ping a user every ten minutes. Here is what I have right now
#client.event
while True:
channel = client.get_channel(717346417592762457)
await message.channel.send(<#!561464966427705374>)
time.sleep(600)
But this results in a lot of errors.
One easy way to do it would be to use discord's tasks extension. You can easily set the amount of time for each loop. The only main drawback is that the loop restarts every time your bot restarts, or rather everytime you run the command, in this example. In the example below, I added the method to start the loop in a command, but you can have it start in an on_ready event or similar.
# New Import
from discord.ext import tasks, commands
# ... Other code ...
#tasks.loop(minutes=10) # #tasks.loop(seconds=600) <-- does the same amount of time
async def ping_loop():
channel = client.get_channel(717346417592762457)
await channel.send("<#!561464966427705374>")
# 1. Don't use message.channel, as you only defined channel and this isn't an event
# 2. As mentioned by Tim Roberts in the comments
# add speech marks to your send() since it's a string
# somewhere else in your code
#client.command()
async def loop(ctx):
try:
ping_loop.start() # start the loop if it isn't currently running
await ctx.send("Started pinging <#!561464966427705374>")
except: # happens if the loop is already running
ping_loop.cancel() # if so, cancel the loop
await ctx.send("Stopped pinging <#!561464966427705374>")
For the specific errors you mentioned:
#client.event
async def somefunction(ctx):
# you need to define a function to call (pass to discord.py)
# also the function has to be async, for the usage of `await`
while True:
channel = client.get_channel(717346417592762457)
await message.channel.send(<#!561464966427705374>)
# not sure if you can mention user like this
await asyncio.sleep(600)
# time.sleep(600) # prefer non-blocking asyncio sleep. make sure `asyncio` is imported
To mention a user, you need to first get user with
user_instance = ctx.guild.get_member(YOUR_USER_discord_id)
Note: You may need to enable the privilege to use the .get_member() function. If you have not enabled it yet, just follow the instructions in the exception.
Then you can sent message with
await ctx.send(f"THE_TEXT_YOU_WANT_TO_SPAM {user_instance.mention}!")
Also, if you are using it in a bot command like me, you may want to add some validation, so that not everyone can cast/invoke the command. I did this with has_role():
#commands.has_role(YOUR_ROLE_ID_THAT_CAN_CAST_THIS_COMMAND)
So in the end, it looks like this:
#bot.command('name': 'spam', 'help':'use at # to mention a user')
#commands.has_role(YOUR_ROLE_ID_THAT_CAN_CAST_THIS_COMMAND)
async def spamMention(ctx, spam_duration_second: int = 3):
user_instance = ctx.guild.get_member(YOUR_USER_discord_id)
for cntr in range(spam_duration_second):
await ctx.send(f"THE_TEXT_YOU_WANT_TO_SPAM {user_instance.mention}!")
await asyncio.sleep(THE_INTERVAL_YOU_WANT_IN_SECOND)
return
It's part of my code so I know it works (my python version: 3.7.9).

How to make a cooldown system with discord.py?

Let's say I have two commands...
hi 1 that sends hi once to user and starts a cooldown of 500 seconds
hi 2 that sends hi twice to user and starts a cooldown of 1000 seconds
Now when I type hi 1 it should not respond to me when I type hi 2.
Something like a shared cooldown system!
How can I do that?
I am currently using this:
#commands.cooldown(1, 5, commands.BucketType.user)
Which allows me to use the multiple commands without stopping for cooldown on a previous command.
You could use a custom check to ensure that the commands aren't used while they're on cooldown.
./q63262849/cooldowns.py
import datetime
from discord.ext import commands
on_cooldown = {} # A dictionary mapping user IDs to cooldown ends
def cooldown(seconds):
def predicate(context):
if (cooldown_end := on_cooldown.get(context.author.id)) is None or cooldown_end < datetime.datetime.now(): # If there's no cooldown or it's over
if context.valid and context.invoked_with in (*context.command.aliases, context.command.name): # If the command is being run as itself (not by help, which runs checks and would end up creating more cooldowns if this didn't exist)
on_cooldown[context.author.id] = datetime.datetime.now() + datetime.timedelta(seconds=seconds) # Add the datetime of the cooldown's end to the dictionary
return True # And allow the command to run
else:
raise commands.CommandOnCooldown(commands.BucketType.user, (cooldown_end - datetime.datetime.now()).seconds) # Otherwise, raise the cooldown error to say the command is on cooldown
return commands.check(predicate)
This can then be imported
./main.py
from q63262849 import cooldowns
and used as a decorator for commands
./main.py
#bot.command()
#cooldowns.cooldown(10)
async def cooldown1(ctx):
await ctx.send("Ran cooldown 1")
#bot.command()
#cooldowns.cooldown(20)
async def cooldown2(ctx):
await ctx.send("Ran cooldown 2")
It is notable that this approach still has some issues, in particular if another later check fails this check will still put the command on cooldown, however these could be solved by putting this check such that it will run after all the other checks.

Categories

Resources