I want the cooldown of one of my commands to start only if a condition in the function is met, like so:
#bot.command
async def move(ctx, destination):
destinations=["d1", "d2", "d3"] # List of valid arguments for the command
if destination in destinations:
movement(destination) # Function to actually move, not important for the question
# Start cooldown only here
else:
await ctx.send("This is not a valid destination")
This way, if the user mistypes the destination, they won't be penalized with the cooldown. How can i achieve that?
EDIT1: one would normally use discord.py's built-in #commands.cooldown decorator, here is the source:
def cooldown(rate, per, type=BucketType.default):
def decorator(func):
if isinstance(func, Command):
func._buckets = CooldownMapping(Cooldown(rate, per, type))
else:
func.__commands_cooldown__ = Cooldown(rate, per, type)
return func
return decorator
However this applies to the whole command.(It is normally placed after the #bot.command decorator)
There could be a lots of ways to craft your own cooldowns, here is a simple one that can do the trick. The idea behind it is for the bot to "remember" the last time someone used this specific command and to check this time before allowing the player to move.
from datetime import datetime, timedelta
on_cooldown = {} # Dictionary with user IDs as keys and datetime as values
destinations=["d1", "d2", "d3"] # List of valid arguments for the command
move_cooldown = 5 # cooldown of the move command in seconds
#bot.command()
async def move(ctx, destination):
if destination in destinations:
author = ctx.author.id
try:
# calculate the amount of time since the last (successful) use of the command
last_move = datetime.now() - on_cooldown[author]
except KeyError:
# the key doesn't exist, the player used the command for the first time
# or the bot has been shut down since
last_move = None
on_cooldown[author] = datetime.now()
if last_move is None or last_move.seconds > move_cooldown:
# move(...)
on_cooldown[author] = datetime.now() # the player successfully moved so we start his cooldown again
await ctx.send("You moved!")
else:
await ctx.send("You're still on cooldown.")
else:
await ctx.send("This is not a valid destination")
Note : you may or may not need to remove the parentheses after the #bot.command decorator.
Idk if this is what you're looking for, but there's a way to make the cooldown only activate after the code parses properly with this bit of code:
#bot.command(cooldown_after_parsing=True)
#commands.cooldown(rate, per, type=<BucketType.default: 0>)
you can find the document for commands.cooldown here
and the doc for cooldown_after_parsing here
Related
I have this function bellow, async def get_all_distinct_identifications(), that if I create it inside CarelinePeopleService class the return of await CareLinePeopleService().enrolled_people_list_in_care_lines() function will be a tuple instead of a Pandas DataFrame which is what it should truly return. I can even change CareLinePeopleService() to self that it still returning tuple instead of a DataFrame.
But if I create this function, async def get_all_distinct_identifications, in another file it returns what is expected from it, a Pandas Dataframe.
I think that it is something that Python, behind the scenes, is messing up for some reason.
Does anyone knows what is going on?
async def get_all_distinct_identifications(insurer_id, trigger_type, conditional_type, conditionals_fields):
df_conditionals = await ConditionalsService().get_from_conditional_table(
insurer_id=insurer_id,
trigger_type=trigger_type,
conditional_type=conditional_type,
fields=conditionals_fields,
care_lines=[],
)
df_of_people_identified = await CareLinePeopleService().enrolled_people_list_in_care_lines(
df_care_line=df_conditionals[['care_line_id']].drop_duplicates().reset_index(drop=True),
identifications=None,
limit=1000,
offset=None
)
return df_of_people_identified
The definition of CareLinePeopleService().enrolled_people_list_in_care_lines is:
async def enrolled_people_list_in_care_lines(self, df_care_line, identifications=None, limit=1000, offset=None):
if type(offset) == int:
df_people_identified = await self.get_batch_of_enrolled_people_by_care_line_id(df_care_line, limit, offset)
elif identifications is not None:
df_people_identified = await self.get_new_enrolled_people_in_care_lines(identifications)
else:
df_people_identified = await self.get_all_enrolled_people_in_care_lines(df_care_line)
return df_people_identified
Definition of all three functions above:
async def get_batch_of_enrolled_people_by_care_line_id(self, df_care_line, limit, offset):
list_of_care_line_id = list(df_care_line['care_line_id'].drop_duplicates().astype(str))
query = CareLinePeopleRepository.enrolled_people_list_in_care_lines(
list_of_care_line_id=list_of_care_line_id,
limit=limit,
offset=offset
)
df_of_people_identified = await Mysql().execute_to_pd(query)
return df_of_people_identified
async def get_new_enrolled_people_in_care_lines(self, identifications):
list_of_care_line_id = list(identifications['care_line_id'].drop_duplicates().astype(str))
identifications_list = list(identifications[['person_id', 'care_line_id']].drop_duplicates().itertuples(index=False, name=None))
query = CareLinePeopleRepository.enrolled_people_list_in_care_lines(
list_of_care_line_id=list_of_care_line_id,
identifications_list=identifications_list
)
df_of_people_identified = await Mysql().execute_to_pd(query)
return df_of_people_identified
async def get_all_enrolled_people_in_care_lines(self, df_care_line):
list_of_care_line_id = list(df_care_line['care_line_id'].drop_duplicates().astype(str))
query = CareLinePeopleRepository.enrolled_people_list_in_care_lines(
list_of_care_line_id=list_of_care_line_id
)
df_of_people_identified = await Mysql().execute_to_pd(query)
return df_of_people_identified
When you say:
...if I create it inside CarelinePeopleService class, ...
If that function (get_all_distinct_identifications) looks like it currently does, it doesn't have self as the first parameter.
Since it's not decorated with something like #staticmethod, depending on how it's called (which you don't specify), the parameters may not be what you think they are.
When you call this via an external file (instead of from within the class), you're likely calling it differently, and passing the parameters in correctly.
So I'm still making a warning system but in the process I am having this issue with the embed showing the warning multiple times. Shown in the image provided.
https://i.stack.imgur.com/ks4Gm.png I'm not sure what could be causing this but I think it might be the for loop I made?
#client.hybrid_command(name = "warnings", with_app_command=True, description="View the warnings of a member", aliases=["punishments"])
async def warnings(ctx, member: discord.Member = None):
if member == None:
await ctx.reply("A Member is required")
else:
check = warndb.warn_logs.find_one({"user_id": member.id})
if check is None:
await ctx.reply("This user has no warns")
else:
reason = check["reason"]
moderator_id = check["moderator_id"]
embed = discord.Embed(color=embedcolor, title=f"{member.name}'s warnings")
for w in check:
embed.add_field(name=f"{reason}", value=f"<#{moderator_id}>", inline=False)
await ctx.send(embed=embed)
There is no error and it works fine it just shows the warning(s) multiple times
You query with find_one so there is no point in looping over check.
I would try to just remove the line for w in check:.
If it's suppose to be an array(change to find_all), maybe you manually adding data to the table when testing and it's displaying all previous warns you've added.
I figured it out finally. The issue is you need to turn the check = warndb.warn_logs.find_one({"user_id": member.id}) into a list by doing check = list(warndb.warn_logs.find_one({"user_id": member.id})). Once you do that you should use the .get() function to get the values of the keys
In this case:
for w in check:
reason = w.get('reason')
moderatorid = w.get('moderator_id')
guildid = w.get('guild_id')
caseid = w.get('_id')
embed.add_field(name=f"Case ID: {caseid}—Reason: {reason}", value=f"<#{moderatorid}>", inline=False)
I would like to display via bot how many times a command has been executed.
For this I have already inquired here on the platform and have tried different things. About my code:
e = discord.Embed(color=discord.Colour.green())
user = ctx.author
e.title = f "New suggestion!"
e.set_footer(text=f"{user} | {user.id}")
e.description = f "**__Submitter:__**\n {ctx.author}"
e.add_field(name="__Suggestion:__", value=f"{text}")
e.set_thumbnail(url=ctx.message.author.avatar_url)
e.timestamp = datetime.utcnow()
await channel.send(embed=e)
await ctx.message.add_reaction("✅")
The bot should add Suggestion no. #number in the title, which will go up one at a time when the command is executed. I have already tried the following:
def __init__(self, bot):
self.bot = bot
self.counter = 0
#Shortened
e.title = f "Suggestion no. {self.counter}"
Or just as well:
e.title = f "Suggestion no. {self.counter + 1}
But that didn't help, so how do I make sure that a correct number is displayed and that in case of a bot restart this number continues to rise and doesn't start again from the beginning.
Note that a global event did not work for me somehow!
You actually need to update the counter variable
self.counter += 1
e.title = f"Suggestion no. {self.counter}"
You can save it in a JSON or text file if you want the variable to continue after the bot restarts
Context:
I am using PyTelegramBotAPi or Python Telegram Bot
I have a code I am running when a user starts the conversation.
When the user starts the conversation I need to send him the first picture and a question if He saw something in the picture, the function needs to wait for the user input and return whether he saw it or not.
After that, I will need to keep sending the picture in a loop and wait for the answer and run a bisection algorithm on it.
What I have tried so far:
I tried to use reply markup that waits for a response or an inline keyboard with handlers but I am stuck because my code is running without waiting for the user input.
The code:
#bot.message_handler(func=lambda msg: msg in ['Yes', 'No'])
#bot.message_handler(commands=['start', 'help'])
def main(message):
"""
This is my main function
"""
chat_id = message.chat.id
try:
reply_answer = message.reply_to_message.text
except AttributeError:
reply_answer = '0'
# TODO : should wait for the answer asynchnonossly
def tester(n, reply_answer):
"""
Displays the current candidate to the user and asks them to
check if they see wildfire damages.
"""
print('call......')
bisector.index = n
bot.send_photo(
chat_id=chat_id,
photo=bisector.image.save_image(),
caption=f"Did you see it Yes or No {bisector.date}",
reply_markup=types.ForceReply(selective=True))
# I SHOUL WAIT FOR THE INPUT HERE AND RETURN THE USER INPUT
return eval(reply_answer)
culprit = bisect(bisector.count, lambda x: x, partial(tester, reply_answer=reply_answer) )
bisector.index = culprit
bot.send_message(chat_id, f"Found! First apparition = {bisector.date}")
bot.polling(none_stop=True)
The algorithm I am running on the user input is something like this :
def bisect(n, mapper, tester):
"""
Runs a bisection.
- `n` is the number of elements to be bisected
- `mapper` is a callable that will transform an integer from "0" to "n"
into a value that can be tested
- `tester` returns true if the value is within the "right" range
"""
if n < 1:
raise ValueError('Cannot bissect an empty array')
left = 0
right = n - 1
while left + 1 < right:
mid = int((left + right) / 2)
val = mapper(mid)
tester_values = tester(val) # Here is where I am using the ouput from Telegram bot
if tester_values:
right = mid
else:
left = mid
return mapper(right)
I hope I was clear explaining the problem, feel free to ask any clarification.
If you know something that can point me in the right direction in order to solve this problem, let me know.
I have tried a similar question but I am not getting answers.
You should save your user info in a database. Basic fields would be:
(id, first_name, last_name, username, menu)
What is menu?
Menu keeps user's current state. When a user sends a message to your bot, you check the database to find out about user's current sate.
So if the user doesn't exist, you add them to your users table with menu set to MainMenu or WelcomeMenu or in your case PictureMenu.
Now you're going to have a listener for update function, let's assume each of these a menu.
#bot.message_handler(commands=['start', 'help'])
so when the user sends start you're going to check user's menu field inside the function.
#bot.message_handler(commands=['start', 'help'])
def main(message):
user = fetch_user_from_db(chat_id)
if user.menu == "PictureMenu":
if message.photo is Not None:
photo = message.photo[0].file_id
photo_file = download_photo_from_telegram(photo)
do_other_things()
user.menu = "Picture2Menu";
user.save();
else:
send_message("Please send a photo")
if user.menu == "Picture2Menu":
if message.photo is Not None:
photo = message.photo[0].file_id
photo_file = download_photo_from_telegram(photo)
do_other_things()
user.menu = "Picture3Menu";
user.save();
else:
send_message("Please send a photo")
...
I hope you got it.
I have found the answer:
the trick was to use next_step_handler, and message_handler_function to handle command starting with start and help
Then as suggested by #ALi in his answer, I will be saving the user input answer as well as the question id he replied to in a dictionary where keys are questions and id are the answer.
Once the user has answered all questions, I can run the algorithms on his answer
Here is how it looks like in the code :
user_dict = {}
# Handle '/start' and '/help'
#bot.message_handler(commands=['help', 'start'])
def send_welcome(message):
# initialise the the bisector and
bisector = LandsatBisector(LON, LAT)
indice = 0
message = send_current_candidate(bot, message, bisector, indice)
bot.register_next_step_handler(
message, partial(
process_step, indice, bisector))
def process_step(indice, bisector, message):
# this run a while loop and will that send picture and will stop when the count is reached
response = message.text
user = User.create_get_user(message, bisector=bisector)
if indice < bisector.count - 1:
indice += 1
try:
# get or create
user.responses[bisector.date] = response # save the response
message = send_current_candidate(bot, message, bisector, indice)
bot.register_next_step_handler(
message, partial(
process_step, indice, bisector))
except Exception as e:
print(e)
bot.reply_to(message, 'oooops')
else:
culprit = bisect(bisector.count,
lambda x: x,
partial(
tester_function,
responses=list(user.responses.values())))
bisector.index = culprit
bot.reply_to(message, f"Found! First apparition = {bisector.date}")
I've written some code using the information here as a guide line,
List users in IRC channel using Twisted Python IRC framework
I can successfully log the NAMES list to the console, however I've so far been unable to
retrieve it to work with it further. Here is an excerpt of the code related to this matter:
class Alfred(irc.IRCClient):
""" An IRC bot """
# Voodoo magic for names
def __init__(self, *args, **kwargs):
self._namescallback = {}
def names(self, channel):
channel = channel.lower()
d = defer.Deferred()
if channel not in self._namescallback:
self. _namescallback[channel] = ([], [])
self._namescallback[channel][0].append(d)
self.sendLine("NAMES %s" % channel)
return d
def irc_RPL_NAMREPLY(self, prefix, params):
channel = params[2].lower()
nicklist = params[3].split(' ')
if channel not in self._namescallback:
return
n = self._namescallback[channel][1]
n += nicklist
def irc_RPL_ENDOFNAMES(self, prefix, params):
channel = params[1].lower()
if channel not in self._namescallback:
return
callbacks, namelist = self._namescallback[channel]
for cb in callbacks:
cb.callback(namelist)
del self._namescallback[channel]
# End of voodoo magic
def get_names(self, nicklist):
# Log the output to the log
log.msg(nicklist)
def has_op(self, channel, user):
self.names('#' + channel).addCallback(self.get_names)
def change_mode(self, channel, user, msg):
target = msg[5:-3]
params = [channel, '+o']
self.has_op(channel, user)
self.mode(channel, True, '+o', user=target)
What I want to achieve is to within the has_op function get a hold of the so called nicklist. So far I've been unable to do this using a trial and error approach, where I've tried to use print and return statements in reasonable places but this has yielded no output what so ever or instance / attribute errors. Obviously, I'm at a loss and I really need some guidance.
The idea of doing this "within" has_op is probably preventing you from making progress.
Make it work at all and then consider how you might improve the factoring of the implementation.
Notice that get_names has the data that you're interested in already. What do you want to do with that information? Check to see if it contains a certain name? For that, you'll need to know which name you're considering. To do this, you can use the feature of Deferred.addCallback that lets you pass an extra argument to the callback:
def get_names(self, nicklist, user):
if user in nicklist:
log.msg("%r has op!" % (user,))
def has_op(self, channel, user):
self.names('#' + channel).addCallback(self.get_names, user)
Now perhaps what you really want isn't to log this information but to make a decision based on it in change_mode (I'm only guessing, of course).
If this is so, then you want to take advantage of another Deferred feature - the ability of each callback attached to a Deferred to process and change the result of the Deferred.
You can change has_op so that instead of logging information it also returns a Deferred - but this Deferred can have a True result if the given user has op in the given channel and a False result of they don't.
def get_names(self, nicklist, user):
return user in nicklist
def has_op(self, channel, user):
return self.names('#' + channel).addCallback(self.get_names, user)
Now you can use this functionality in change_mode:
def change_mode(self, channel, user, msg):
target = msg[5:-3]
checking = self.has_op(channel, user)
checking.addCallback(self._ofIfNotOp, channel, target)
def _opIfNotOp(self, isOp, channel, user):
if not isOp:
self.mode(channel, True, '+o', user=user)