Discord Bot Looping Task from Command - python

Good afternoon all,
I should preface this post with the statement that I am pretty new to Python and know enough to get myself into trouble, but not enough to always get out of trouble...this is one of those instances I can't get out.
I am attempting to create a Discord Bot using discord.py that has the ultimate goal of being started from a command in Discord, starts counting the days since the start command was issued, and sends a message to Discord every morning with the count. I also would have a command to reset the count and another to cancel the counter.
For testing purposes, I have created code to count minutes instead of days and a task loop of only 10 seconds rather than a full day.
My problem is that I am attempting to use a discord.ext task for the loop, but I clearly don't know how to use it properly, and my research online has not made it any more clear to me. I'm hoping some folks here can steer me in the right direction. I have supplied my code below.
What I expect to happen when I execute my code:
I issue the $start arg command and the bot sends a message of "It has been X minutes"
Every 10 seconds the bot sends the same message until 1 minute has passed, then message reads "It has been X+1 minutes"
Continue looping until canceled
What actually happens:
I issue the $start arg command and the bot sends a message of "It has been X minutes"
Nothing else. No other messages or anything happens in Discord, no errors shown in the console.
The code is currently being hosted on replit.com, hence the keep_alive function using UpTimeRobot to keep the bot alive.
Something to note, I originally used asyncio.sleep() to just wait to send the new message. This worked fine for shorter periods (like a few minutes to hours) but even with UpTimeRobot doing its thing, I can't get 100% uptime of the Bot and I think whenever the bot went offline for a few minutes, it stopped my counter loop and that was that. This is what lead me to look into Tasks, as I read they can be used to continue loops on reconnection if the bot goes offline for a bit.
import discord
import os
import keep_alive
from datetime import date, datetime, time, timedelta
from discord.ext import commands, tasks
bot = commands.Bot(command_prefix="$")
init_count = 0
count = 0
channel_id = My_Channel_ID_Here
#bot.event
async def on_ready():
print('We have logged in as {0.user}'.format(client))
#tasks.loop(seconds=10, count=None, reconnect=True)
async def min_counter():
global count
global init_count
count = init_count
today = datetime.today()
now = datetime.today()
channel = bot.get_channel(channel_id)
if today.minute != now.minute:
# increment count by 1
count = count + 1
today = now
print('Count: ' + str(count))
await channel.send('It Has Been ' + str(count) + ' Minutes.') # Send message of minutes count
#bot.command() # Command to start the counter
async def start (ctx, arg: int): # arg is initial number of minutes to start with
global init_count
init_count = arg
await ctx.send('It Has Been ' + str(init_count) + ' Minutes.') # Send message of minutes count
min_counter.start()
#bot.command() # Command to reset counter
async def reset (ctx):
global count
count = 0
await ctx.send('It Has Been 0 Minutes.') # Send message that count is not zero
#bot.command() # Command to stop the counter
async def stop (ctx):
min_counter.cancel()
await ctx.send('Counter Stopped.')
keep_alive.keep_alive() # keep alive function call
bot.run(os.getenv('TOKEN')) # Discord bot private token call

Upon quick inspection you are looping the method min_counter() but every time it is called you are changing the values of now and today to datetime.today().
So when you go to compare the values here:
if today.minute != now.minute:
# increment count by 1
count = count + 1
today = now
print('Count: ' + str(count))
await channel.send('It Has Been ' + str(count) + ' Minutes.') # Send message of minutes count
This will always evaluate to False.
Easy Fix
Although I don't like the use of globals too much, here is how we could fix this keeping the style you have now.
Start the counter and initialize a variable named start_time in global scope:
#bot.command() # Command to start the counter
async def start (ctx, arg: int): # arg is initial number of minutes to start with
global init_count, start_time
start_time = datetime.today()
await ctx.send('It Has Been ' + str(init_count) + ' Minutes.') # Send message of minutes count
min_counter.start()
Then check if start_time is equal to now in the loop:
#tasks.loop(seconds=10, count=None, reconnect=True)
async def min_counter():
global count, start_time, init_count
now = datetime.today()
channel = bot.get_channel(channel_id)
if start_time != now:
# increment count by 1
count = count + 1
print('Count: ' + str(count))
await channel.send('It Has Been ' + str(count) + ' Minutes.') # Send message of minutes count
Let me know if everything works for you, I'd be happy to help further if needed.

Related

Unix Time not displaying correctly | NEXTCORD

I have a bot with a giveaway command.
Info
API: Discord API via Nextcord
Language: Python
The module that interprets time (from human time [1m | 1h | 1d] to seconds): humanfriendly
Expectations
I wanted a sort of... uh... a "ends in" thing. Discord has a built-in Unix Time thingy that's syntax is this:
<t:UNIX:type>
Unix is the UNIX time, type is the- well... type. E.g., R as relative time
Result
Well, I met this when I used 1m (1 minute):
Ends in [some whole 2 months!]
Yes, it appeared as two months there. The actual time was working correctly. The giveaway can end in one minute. But my problem is with the time display.
Code
#commands.command(name="gstart")
#commands.has_permissions(manage_guild=True)
async def gquickStart(self, ctx, gtime, *, gprize: str):
if gtime == None:
return await ctx.send("Include a time.")
elif gprize == None:
return await ctx.send("How are we gonna giveaway nothing?")
gawtime = humanfriendly.parse_timespan(gtime)
gawtimetuple = nextcord.utils.utcnow() + datetime.timedelta(gawtime)
gwembed = nextcord.Embed(
title=f"**:tada: {gprize} :tada:**",
description=f"Ends in <t:{int(time.mktime(gawtimetuple.timetuple()))}:R> <t:{int(time.mktime(gawtimetuple.timetuple()))}:T> \n {ctx.author.mention} is giving away **{gprize}**!",
color=nextcord.Colour.green())
gwemend = nextcord.Embed(
title=f"**:tada: GIVEAWAY ENDED :tada:**",
description = f"{ctx.author.mention} has gave away **{gprize}**!",
color=nextcord.Colour.red()
)
gwembed.set_footer(text=f"Giveaway ends in {gtime}")
gaw_msg = await ctx.send(embed=gwembed)
await ctx.message.delete()
await gaw_msg.add_reaction('๐ŸŽ‰')
await asyncio.sleep(gawtime)
global new_gaw_msg
new_gaw_msg = await ctx.channel.fetch_message(gaw_msg.id)
global users
users = await new_gaw_msg.reactions[0].users().flatten()
users.pop(users.index(client.user))
try:
winner = random.choice(users)
await ctx.send(f"{winner.mention} has won the giveaway for **{gprize}**")
await new_gaw_msg.edit(embed=gwemend)
except IndexError:
await new_gaw_msg.reply("1 Winner needed for the giveaway, 0 provided")
await new_gaw_msg.edit(embed=gwemend)
BTW, if it matters, I use the command in a category.
Please answer if you can.
Thanks in advance,
Beedful
Explanation
When constructing the timedelta, the first positional argument is days. So, by adding timedelta(60) to utcnow, you add 60 days to the current time.
An easier method would be simply to convert utcnow to a float with .timestamp(), then sum that with gawtime.
Code
#commands.command(name="gstart")
#commands.has_permissions(manage_guild=True)
async def gquickStart(self, ctx, gtime, *, gprize: str):
if gtime is None:
return await ctx.send("Include a time.")
elif gprize is None:
return await ctx.send("How are we gonna giveaway nothing?")
gawtime = humanfriendly.parse_timespan(gtime)
end_time = nextcord.utils.utcnow().timestamp() + gawtime
gwembed = nextcord.Embed(
title=f"**:tada: {gprize} :tada:**",
description=f"Ends in <t:{int(end_time)}:R> <t:{int(end_time)}:T> \n {ctx.author.mention} is giving away **{gprize}**!",
color=nextcord.Colour.green()
)
Unrelated: From PEP 8
Comparisons to singletons like None should always be done with is or is not, never the equality operators.
Reference
parse_timestamp
utcnow
timedelta

Discord.py - remind command with multiple time included

I'm trying to make a discord bot with a remind command. I would like the remind command to be able to do like ;remind 12.5m or ;remind 1h 12m 30s. but right now the remind command can only be used like ;remind 45m, ;remind 2h, ;remind 9m. basically the command only works if only one time is included. I'm not sure how to make the command work with multiple time included, can someone help me?
#client.command(case_insensitive=True, aliases=["reminder", "rm"])
async def remind(ctx, time, *, reminder="something"):
user = ctx.author.mention
channel = client.get_channel(12345678912345)
seconds = 0
log = discord.Embed(color=0xe9a9a9, timestamp=datetime.utcnow())
embed = discord.Embed(color=0xe9a9a9)
if time.lower().endswith("d"):
seconds += int(time[:-1]) * 60 * 60 * 24
counter = f"{seconds // 60 // 60 // 24} days"
if time.lower().endswith("h"):
seconds += int(time[:-1]) * 60 * 60
counter = f"{seconds // 60 // 60} hours"
if time.lower().endswith("m"):
seconds += int(time[:-1]) * 60
counter = f"{seconds // 60} minutes"
if time.lower().endswith("s"):
seconds += int(time[:-1])
counter = f"{seconds} seconds"
if seconds == 0 or seconds > 7776000:
await ctx.send("Please specify a valid time.")
if reminder is None:
await ctx.send("Please tell me what to remind you.")
else:
log.set_author(name=f"{ctx.author.display_name}#{ctx.author.discriminator} - Remind", icon_url=ctx.author.avatar_url)
log.set_footer(text=f"{counter} | {reminder}")
embed.add_field(
name="**Reminder**",
value=f"{user}, you asked me to remind you to `{reminder}` `{counter}` ago."
)
await ctx.send(f"Alright, I will remind you about `{reminder}` in `{counter}`.")
await asyncio.sleep(seconds)
await ctx.author.send(embed=embed)
await channel.send(embed=log)
return
When you do
remind(ctx, time, *, reminder='something')
The * bit is saying that when you call that function any parameters after that point have to be a named ones.
Instead if put the * in front of time then you are triggering a completely different behavior, and time will become a list of all of the different times given to you. Like so.
async def remind(ctx, *times, reminder="something"):
for time in times:
# move the code here
# Getting rid of the return statement at the end
However it will run each timer sequentially, waiting to count down the second one until the first one is finished. If you want to start all of the timers at once, since we're already using asyncio, there is a way we can easily accomplish this with asyncio.gather.
async def remind(ctx, *times, reminder="something"):
asyncio.gather(*[_remind(ctx, time, reminder) for time in times])
async def _remind(ctx, time, reminder):
# All of the code that was in your original remind runction
# would go here instead
Bytheway, once again the star (the one in async.gather) is acting in a different way.
Looks like you are using * for the reminder, meaning all provided times after the first one will be stored in your reminder variable. You can search the reminder variable for and valid times at the very start of the string.

How can I get the number of messages sent by a user in a discord server and store it in a list [Discord.py]

I'm currently using this code to check the number of messages sent by a user but this approach is very slow, it is taking 1 - 2 min to calculate for each user
user = discord.utils.find(lambda m: m.id== j, channel.guild.members)
async for message in channel.history(limit = 100000):
if message.author == user:
userMessages.append(message.content)
print(len(userMessages))
is there any other fast approach to doing this?
Counting messages
You can use on_message event to count messages.
message_count = {}
#client.event
async def on_message(message):
global message_count
if message.guild.id not in message_count:
message_count[message.guild.id] = {}
try:
message_count[message.guild.id][message.author.id] += 1
except KeyError:
message_count[message.guild.id][message.author.id] = 1
client.process_commands(message)
And then use
member = something # specify member here
try:
count = message_count[member.guild.id][member.id]
except KeyError:
count = 0
# now `count` is count of messages from `member`
To get count of messages from member.
Note: Message count resets on your bot restart but this solution would work very quickly.
Database
Another way to do what you want is use any database to store message count from different members.

How can i create a reminder command in discord.py?

I want to create a reminder command in discord.py, this command will remind a specific role that an event is coming. For that I saved in a first variable the hours when the event starts and in a second variable I subtracted the hours when the event starts and the minutes in advance.
Then I put the second variable in the async.io function so that the reminder is only given at that time.
For example if the event starts at 20:00 and the advance time is 2 the bot should remind at 19:58
But this does not work and I get the following error on the console:
discord.ext.commands.errors.CommandInvokeError: Command raised an exception: TypeError: int() argument must be a string, a bytes-like object or a number, not 'datetime.timedelta'
Heres the code I have:
#bot.command()
async def reminder(ctx, arg):
if arg == 'sap':
role = 839182749452992639
message = 'hello '
event_start_time = timedelta(hours=20, minutes=0)
advance_time = event_start_time - timedelta(minutes=2)
await asyncio.sleep(int(advance_time) * 60)
role = get(ctx.guild.roles, id=int(role))
for user in ctx.guild.members:
if role in user.roles:
userDM = await user.create_dm() if (user.dm_channel is None) else user.dm_channel
if userDM != None:
await userDM.send(message + user.name)
You cannot really cast a datetime.timedelta object into an integer, if you want to get the total seconds just use the total_seconds() method
await asyncio.sleep(advance_time.total_seconds()))
Your logic is off though, the function itโ€™s not going to sleep till 19:58, but for 19 hours and 58 minutes, to fix it you have to subtract the current time to the desired time and get the seconds
now = datetime.now() # or utcnow
future = datetime(now.year, now.month, now.day, 19, 58)
delta = (future - now).total_seconds()
await asyncio.sleep(delta)
Your following lines will produce a timedelta object:
event_start_time = timedelta(hours=20, minutes=0)
advance_time = event_start_time - timedelta(minutes=2)
when you are trying to do int(advance_time) this produces your error, what you can do is to get the seconds directly from the timedelta object:
event_start_time = timedelta(hours=20, minutes=0)
advance_time = event_start_time - timedelta(minutes=2)
advance_time_seconds = advance_time.seconds
# you already have the difference in seconds, no need to multiply by 60
# can keep the int() so yo get and integer
await asyncio.sleep(int(advance_time))
I already made a similar command before, so try this:
Example usage
#client.command(name='timer', help='Create a timer.', usage='<time> [s|m|h] (<message>)')
async def timer(ctx, time, unit, *message):
if not message:
message = 'Time\'s up!'
else:
message = ' '.join(message)
time = int(time)
oldtime = time # time without unit calculation to seconds
if unit == 's':
pass
elif unit == 'm':
time *= 60
elif unit == 'h':
time *= 3600
else:
await ctx.send(':x: **ERROR**: Invalid unit.\nUnit can be \'s\' for seconds, \'m\' for minutes or \'h\' for hours.')
return
if time > 10000:
await ctx.send(':x: **ERROR**: Timer can\'t be longer than 10000 seconds.')
return
await ctx.send(f':white_check_mark: Creating a timer for **{oldtime}{unit}** with message \"**{message}**\"...\nYou will get mentioned/pinged.')
await asyncio.sleep(time)
await ctx.send(f'{ctx.author.mention} **{message}** (**{oldtime}{unit}** passed.)')

Discord.py reactions counting

I wanted, to make giveaway command. I know how to do this, but there's one problem, i don't know how to make my bot count reactions. I maked a command, bot sends message, adds reactions, sleep x time, and then it must to count reactions and choose winner. How do i do this?
send message
wait x seconds
fetch the message again
count the total reactions
choose a random reaction
choose a random user
# 1
message = await ctx.send('Waiting x seconds')
# 2
await asyncio.sleep(x)
# 3
message = await ctx.channel.fetch_message(message.id)
# 4
total_count = 0
for r in message.reactions:
total_count += r.count
# 5
reaction = random.choice(message.reactions)
# 6
members = await reaction.users().flatten()
winner = random.choice(members)

Categories

Resources