1

So, I'm trying to create a reminder function using discord.py. This is my code:

@client.command(name = "reminder", brief = "I'll send a message to the future.",
                description = "State what I have to remind you, as well as when and in which channel and I'll do it! "
                             "Write d for days, h for hours, m for minutes and s for seconds after the number and then "
                              "what you want to be remided of. For example: >reminder 1h 30m eat chocolate",
                aliases = ["remind", "remindme", "remindto"])

async def reminder(ctx, time, *, reminder):
    user = ctx.message.author
    seconds = 0
    if reminder is None:
        ctx.send("Please, tell me what you want me to remind you about!")
    if time.lower().endswith("d"):
        seconds += float(time[:-1]) * 60 * 60 * 24
        counter = f"{seconds // 60 // 60 // 24} days"
    if time.lower().endswith("h"):
        seconds += float(time[:-1]) * 60 * 60
        counter = f"{seconds // 60 // 60} hours"
    if time.lower().endswith("m"):
        seconds += float(time[:-1]) * 60
        counter = f"{seconds // 60} minutes"
    if time.lower().endswith("s"):
        seconds += float(time[:-1])
        counter = f"{seconds} seconds"
    if seconds == 0:
        await ctx.send("You can't tell me that!")

    else:
        await ctx.send(f"Alright, I will remind you about {reminder} in {counter}.")
        await asyncio.sleep(seconds)
        await ctx.send(f"Hi, <@{user.id}>, you asked me to remind you about {reminder} {counter} ago.")
        return

My issue is that I have no clue how to make it work when someone write more than one argument for "time". So for instance, if I call the function >reminder 1h 30min eat chocolate, it will remind me in 1h to "30min eat chocolate", instead of reminding me in 1h 30min. I don't know if there is a way to fix this (apart from writing 1.5h). Any input will be useful. Thanks a lot!

Kali
  • 65
  • 1
  • 9
  • nothing in this code contains the "parsing" of the usermessage into `time` and `reminder` - the function already gets the message as splitted values. Try a message of `'reminder 1h30min eat chocolate'` and see if gets split correctly, else post the relevant code that takes the message of the user apart and calls `async def reminder(ctx, time, *, reminder):` with parametrs – Patrick Artner Sep 27 '20 at 09:56
  • Writing 1h30m doesn't work either. And I don't fully understand the last sentence? I posted all my code for the function. – Kali Sep 27 '20 at 10:06

2 Answers2

0

The message your user in inputting is "somewhere" split into parameters that then call the async method you posted. The "splitting" of the user input is NOT done in the code you showed.

If you want to handle '1d 3hours 52sec Check StackOverflow for answers.' as user input you need to change the way this message is split before calling async def reminder(ctx, time, *, reminder) with the split parameters.

You need to change it so that time gets provided as '1d 3hours 52sec' and reminder is provided as 'Check StackOverflow for answers.':

# correctly proivided params
reminder(ctx, "1d 3hours 52sec", *, reminder="Check StackOverflow for answers")

If you are unable to change this splitting behaviour, forbid spaces inside the time-component of the message the user uses:

It seems that your users text is currently split at spaces and the first is provided for time and the remainder as reminder message.

If you forbid spaces inside time-components you can change your method to parse the time correctly for inputs like '1d3hours52sec Check StackOverflow for answers.'.

This could be a -non async- implementation of a method that handles above messages:

Some helper methods and imports:

imort time 

def format_seconds(secs):
    # mildly adapted from source: https://stackoverflow.com/a/13756038/7505395 
    # by Adam Jacob Muller
    """Accepts an integer of seconds and returns a minimal string formatted into
    'a years, b months, c days, d hours, e minutes, f seconds' as needed."""
    periods = [
        ('year',        60*60*24*365),
        ('month',       60*60*24*30),
        ('day',         60*60*24),
        ('hour',        60*60),
        ('minute',      60),
        ('second',      1)
    ]

    strings=[]
    for period_name, period_seconds in periods:
        if secs > period_seconds:
            period_value , secs = divmod(secs, period_seconds)
            has_s = 's' if period_value > 1 else ''
            strings.append("%s %s%s" % (period_value, period_name, has_s))

    return ", ".join(strings)

def convert_timestring_to_seconds(time):
    """Convert a math expression to integer,only allows [0..9+* ] as chars."""
    if not all(c in "1234567890+* " for c in time):
        raise Exception()
    # this are seconds - using eval as we can be sure nothing harmful can be in it
    # if you are paranoid, use https://stackoverflow.com/a/33030616/7505395 instead
    count = eval(time)
    if type(count) != int:
        raise Exception()
    return count

a mock for your methods context object:

class MockContext:
    def __init__(self):
        class User:
            def __init__(self):
                self.id = "mee-id"
            def __str__(self):
                return "mee"
        class Message:
            def __init__(self):
                self.author = User() 
        self.message = Message()
        self.send = print 

and your changed-up method (non-async and mocked for minimal reproducible example purposes):

def reminder(ctx, time, *, reminder):
    user = ctx.message.author

    if not time:
        ctx.send("Please, tell me WHEN you want me to remind you!")
        return
    elif not reminder:
        ctx.send("Please, tell me WHAT you want me to remind you about!")
        return

    # order and 3.7+ is important - (else use the sorting one down below)
    replacer = {"days":24*60*60, "hours":60*60, "minutes":60, "min":60, 
                "seconds":1, "sec":1, "d":24*60*60, "h":60, "m":60, "s":1}
    safe_time = time # for error output we remember the original input for time

    for unit in replacer: # or sorted(replacer, key=len, reverse=True) below 3.8 
        time = time.replace(unit, f"*{replacer[unit]}+")
    time = time.rstrip("+")

    try:
        count = convert_timestring_to_seconds(time)
    except Exception as ex:
        ctx.send(f"Unable to understand the time of '{safe_time}'!", ex)
        return

    if count == 0:
        ctx.send("You can't tell me that!") 
    else:
        counter = format_seconds(count)
        ctx.send(f"Alright, I will remind you about '{reminder}' in '{counter}'.")
        import time
        time.sleep(2)
        ctx.send(f"Hi, <@{user.id}>, you asked me to remind you about '{reminder}' some '{counter}' ago.")

This is callable like so:

context_mock = MockContext() 

reminder(context_mock, "1d5min270seconds", reminder = "abouth this") 
reminder(context_mock, "1d5min270seconds", reminder = "")
reminder(context_mock, "", reminder = "")

reminder(context_mock, "buffalo", reminder = "abouth this") 

and produces the following outputs:

# reminder(context_mock, "1d5min270seconds", reminder = "abouth this") 
Alright, I will remind you about 'abouth this' in '1 day, 9 minutes, 30 seconds'.
<2s delay>
Hi, <@mee-id>, you asked me to remind you about 'abouth this' some '1 day, 9 minutes, 30 seconds' ago.

# reminder(context_mock, "1d5min270seconds", reminder = "")
Please, tell me WHAT you want me to remind you about!

# reminder(context_mock, "", reminder = "")
Please, tell me WHEN you want me to remind you!

# reminder(context_mock, "buffalo", reminder = "abouth this")
Unable to understand the time of 'buffalo'! 
Patrick Artner
  • 50,409
  • 9
  • 43
  • 69
  • I honestly didn't understand much of your answer. I'm very new to python and discord bots. – Kali Sep 27 '20 at 14:29
0

This is how I ended up fixing the issue:

async def reminder(ctx):
    user = ctx.message.author
    def check(msg):
        return msg.author == ctx.author and msg.channel == ctx.channel
               #and type(msg.content) == int
    await ctx.send("Oh my! Yes, tell me, what do you need to remember?")
    reminder = await client.wait_for("message", check=check)

    await ctx.send("Okay! When do you want me to remind you that? (d for days, h for hours, m for minutes, s for seconds)")
    Time = await client.wait_for("message", check=check)
    times = str(Time.content)
    Times = times.split()

    seconds = 0

    for time in Times:
        if time.lower().endswith("d"):
            seconds += float(time[:-1]) * 60 * 60 * 24
            counter = f"{seconds // 60 // 60 // 24} days"
        if time.lower().endswith("h"):
            seconds += float(time[:-1]) * 60 * 60
            counter = f"{seconds // 60 // 60} hours"
        if time.lower().endswith("m"):
            seconds += float(time[:-1]) * 60
            counter = f"{seconds // 60} minutes"
        if time.lower().endswith("s"):
            seconds += float(time[:-1])
            counter = f"{seconds} seconds"
        if seconds == 0:
            await ctx.send("You can't tell me that!")

    else:
        await ctx.send(f"Alright, I will remind you about {reminder.content} in {times}.")
        await asyncio.sleep(seconds)
        await ctx.send(f"Hi, <@{user.id}>, you asked me to remind you about {reminder.content} some time ago. "
                   f"Don't forget about it!")

So, instead of messing with the arguments of the function, the bot will be asking the user for the reminder and the time separately.

Kali
  • 65
  • 1
  • 9