示例#1
0
    async def undo(self, ctx: Context, *, count: str = None):
        """Undo/remove your latest poms.

        Optionally specify a number to undo that many poms.
        """
        _count = 1

        if count:
            first_word, *_ = count.split(" ", 1)

            try:
                _count = int(first_word)
            except ValueError:
                await ctx.message.add_reaction(Reactions.WARNING)
                await ctx.send(f"Please specify a number of poms to undo.")
                return

            if not 0 < _count <= Config.POM_TRACK_LIMIT:
                await ctx.message.add_reaction(Reactions.WARNING)
                await ctx.send("You can only undo between 1 and "
                               f"{Config.POM_TRACK_LIMIT} poms at once.")
                return

        Storage.delete_most_recent_user_poms(ctx.author, _count)
        await ctx.message.add_reaction(Reactions.UNDO)
示例#2
0
    async def on_ready(self):
        """Startup procedure after bot has logged into Discord."""
        _log.info("MYSQL_DATABASE: %s", Secrets.MYSQL_DATABASE)

        active_channels = ", ".join(f"#{channel}"
                                    for channel in Config.POM_CHANNEL_NAMES)
        _log.info("POM_CHANNEL_NAMES: %s", active_channels or "ALL CHANNELS")

        debug_options_enabled = ", ".join(
            [k for k, v in vars(Debug).items() if v is True])
        if debug_options_enabled:
            debug_enabled_message = textwrap.dedent(f"""\
                ************************************************************
                DEBUG OPTIONS ENABLED: {debug_options_enabled}
                ************************************************************\
            """)

            for line in debug_enabled_message.split("\n"):
                _log.info(line)

        Storage.create_tables_if_not_exists()

        if Debug.DROP_TABLES_ON_RESTART:
            if not __debug__:
                msg = ("This bot is unwilling to drop tables in production. "
                       "Either review your configuration or run with "
                       "development settings (use `make dev`).")

                await self.bot.close()
                raise RuntimeError(msg)

            Storage.delete_all_rows_from_all_tables()

        _log.info("READY ON DISCORD AS: %s", self.bot.user)
示例#3
0
    async def newleaf(self, ctx: Context):
        """Turn over a new leaf.

        Hide the details of your previously tracked poms and start a new
        session.
        """
        Storage.clear_user_session_poms(ctx.author)

        await ctx.send("A new session will be started when you track your "
                       f"next pom, <@!{ctx.author.id}>")
        await ctx.message.add_reaction(Reactions.LEAVES)
示例#4
0
    async def events(self, ctx: Context):
        """See the current and next events."""
        reported_events = []

        try:
            ongoing_event, *_ = Storage.get_ongoing_events()
        except ValueError:
            pass
        else:
            await send_embed_message(
                ctx,
                title="Ongoing Event!",
                description=textwrap.dedent(f"""\
                    Event name: **{ongoing_event.event_name}**
                    Poms goal:  **{ongoing_event.pom_goal}**

                    Started:  *{ongoing_event.start_date.strftime("%B %d, %Y")}*
                    Ending:   *{ongoing_event.end_date.strftime("%B %d, %Y")}*
                """),
            )

            reported_events.append(ongoing_event)

        try:
            upcoming_event, *_ = [
                event for event in Storage.get_all_events()
                if event.start_date > datetime.now()
            ]
        except ValueError:
            pass
        else:
            await send_embed_message(
                ctx,
                title="Upcoming Event!",
                description=textwrap.dedent(f"""\
                    Event name: **{upcoming_event.event_name}**
                    Poms goal:  **{upcoming_event.pom_goal}**

                    Starts:  *{upcoming_event.start_date.strftime("%B %d, %Y")}*
                    Ends:    *{upcoming_event.end_date.strftime("%B %d, %Y")}*
                """),
            )

            reported_events.append(upcoming_event)

        if not any(reported_events):
            await send_embed_message(
                ctx,
                title="Events!",
                description="No ongoing or upcoming events :confused:",
            )
示例#5
0
    async def total(self, ctx: Context):
        """Allows guardians and helpers to see the total amount of poms
        completed by KOA users since ever.

        This is an admin-only command.
        """
        num_poms = Storage.get_num_poms_for_all_users()
        await ctx.send(f"Total amount of poms: {num_poms}")
示例#6
0
    async def do_remove_event(self, ctx: Context, *args):
        """Allows guardians and helpers to start an event.

        This is an admin-only command.
        """

        if not args:
            cmd = ctx.prefix + ctx.invoked_with

            await ctx.author.send(textwrap.dedent(f"""
                Remove an event.
                ```text
                Usage: {cmd} <name>
                ```
            """))

            return

        name = " ".join(args).strip()
        Storage.delete_event(name)
        await ctx.message.add_reaction(Reactions.CHECKMARK)
示例#7
0
    async def poms(self, ctx: Context):
        """Show your poms.

        See details for your tracked poms and the current session.
        """
        poms = Storage.get_all_poms_for_user(ctx.author)
        title = f"Pom statistics for {ctx.author.display_name}"

        if not poms:
            await send_embed_message(
                ctx,
                title=title,
                description="You have no tracked poms.",
                private_message=True,
            )
            return

        session_poms = [pom for pom in poms if pom.is_current_session()]

        descriptions = [pom.descript for pom in session_poms if pom.descript]
        session_poms_with_description = Counter(descriptions)

        num_session_poms_without_description = len(session_poms) - sum(
            n for n in session_poms_with_description.values())

        await send_embed_message(
            ctx,
            private_message=True,
            title=title,
            description=textwrap.dedent(f"""\
                **Pom statistics**
                Session started: *{_get_duration_message(session_poms)}*
                Total poms this session: *{len(session_poms)}*
                Accumulated poms: *{len(poms)}*

                **Poms this session**
                {os.linesep.join(f"{desc}: {num}"
                    for desc, num in session_poms_with_description.most_common())
                    or "*No designated poms*"}
                {f"Undesignated poms: {num_session_poms_without_description}"
                    if num_session_poms_without_description
                    else "*No undesignated poms*"}
            """),
        )
示例#8
0
    async def howmany(self, ctx: Context, *, description: str = None):
        """List your poms with a given description."""
        if description is None:
            await ctx.message.add_reaction(Reactions.WARNING)
            await ctx.send("You must specify a description to search for.")
            return

        poms = Storage.get_all_poms_for_user(ctx.author)
        matching_poms = [pom for pom in poms if pom.descript == description]

        if not matching_poms:
            await ctx.message.add_reaction(Reactions.WARNING)
            await ctx.send("You have no tracked poms with that description.")
            return

        await ctx.message.add_reaction(Reactions.ABACUS)
        await ctx.send('You have {num_poms} *"{description}"* pom{s}.'.format(
            num_poms=len(matching_poms),
            description=description,
            s="" if len(matching_poms) == 1 else "s",
        ))
示例#9
0
    async def do_start_event(self, ctx: Context, *args):
        """Allows guardians and helpers to start an event.

        This is an admin-only command.
        """
        def _usage(header: str = None):
            cmd = ctx.prefix + ctx.invoked_with

            header = (header
                      or f"Your command `{cmd + ' ' + ' '.join(args)}` does "
                      "not meet the usage requirements.")

            return textwrap.dedent(f"""\
                {header}
                ```text
                Usage: {cmd} <name> <goal> <start_month> <start_day> <end_month <end_day>

                Where:
                    <name>         Name for this event.
                    <goal>         Number of poms to reach in this event.
                    <start_month>  Event starting month.
                    <start_day>    Event starting day.
                    <end_month>    Event ending month.
                    <end_day>      Event ending day.

                Example:
                    {cmd} The Best Event 100 June 10 July 4

                At present, events must not overlap; only one concurrent event
                can be ongoing at a time.
                ```
            """)

        try:
            *name, pom_goal, start_month, start_day, end_month, end_day = args
        except ValueError:
            await ctx.message.add_reaction(Reactions.ROBOT)
            await ctx.author.send(_usage())
            return

        event_name = " ".join(name)

        try:
            pom_goal = int(pom_goal)

            if pom_goal <= 0:
                raise ValueError("Goal must be a positive number.")
        except ValueError as exc:
            await ctx.message.add_reaction(Reactions.ROBOT)
            await ctx.author.send(_usage(f"Invalid goal: `{pom_goal}`, {exc}"))
            return

        dateformat = "%B %d %Y %H:%M:%S"
        year = datetime.today().year
        dates = {
            "start": f"{start_month} {start_day} {year} 00:00:00",
            "end": f"{end_month} {end_day} {year} 23:59:59",
        }

        for date_name, date_str in dates.items():
            try:
                dates[date_name] = datetime.strptime(date_str, dateformat)
            except ValueError:
                await ctx.message.add_reaction(Reactions.ROBOT)
                await ctx.author.send(_usage(f"Invalid date: `{date_str}`"))
                return

        start_date, end_date = dates.values()

        if end_date < start_date:
            end_date = datetime.strptime(
                f"{end_month} {end_day} {year + 1} 23:59:59", dateformat)

        overlapping_events = Storage.get_overlapping_events(start_date, end_date)

        if any(overlapping_events):
            msg = "Found overlapping events: {}".format(
                ", ".join(event.event_name for event in overlapping_events)
            )
            await ctx.message.add_reaction(Reactions.ROBOT)
            await ctx.author.send(_usage(msg))
            return

        try:
            Storage.add_new_event(event_name, pom_goal, start_date, end_date)
        except pombot.errors.EventCreationError as exc:
            await ctx.message.add_reaction(Reactions.ROBOT)
            await ctx.author.send(_usage(f"Failed to create event: {exc}"))
            return

        State.goal_reached = False

        await send_embed_message(
            ctx,
            title="New Event!",
            description=textwrap.dedent(f"""\
                Event name: **{event_name}**
                Poms goal:  **{pom_goal}**

                Starts: *{start_date.strftime("%B %d, %Y")}*
                Ends:   *{end_date.strftime("%B %d, %Y")}*
            """),
        )
示例#10
0
    async def pom(self, ctx: Context, *, description: str = None):
        """Add a new pom.

        If the first word in the description is a number (1-10), multiple
        poms will be added with the given description.

        Additionally, find out if there is an ongoing event, and, if so, mark
        the event as completed if this is the final pom in the event.
        """
        count = 1

        if description:
            head, *tail = description.split(" ", 1)

            try:
                count = int(head)
            except ValueError:
                pass
            else:
                if not 0 < count <= Config.POM_TRACK_LIMIT:
                    await ctx.message.add_reaction(Reactions.WARNING)
                    await ctx.send("You can only add between 1 and "
                                   f"{Config.POM_TRACK_LIMIT} poms at once.")
                    return

                description = " ".join(tail)

            if len(description) > Config.DESCRIPTION_LIMIT:
                await ctx.message.add_reaction(Reactions.WARNING)
                await ctx.send("Your pom description must be fewer than "
                               f"{Config.DESCRIPTION_LIMIT} characters.")
                return

        has_multiline_description = description is not None and "\n" in description

        if has_multiline_description and Config.MULTILINE_DESCRIPTION_DISABLED:
            await ctx.message.add_reaction(Reactions.WARNING)
            await ctx.send("Multi-line pom descriptions are disabled.")
            return

        Storage.add_poms_to_user_session(ctx.author, description, count)
        await ctx.message.add_reaction(Reactions.TOMATO)

        if State.goal_reached:
            return

        try:
            ongoing_event, *other_ongoing_events = Storage.get_ongoing_events()
        except ValueError:
            # No ongoing events.
            return

        if any(other_ongoing_events):
            msg = "Only one ongoing event supported."
            raise pombot.errors.TooManyEventsError(msg)

        num_current_poms_for_event = Storage.get_num_poms_for_date_range(
            ongoing_event.start_date, ongoing_event.end_date)

        if num_current_poms_for_event >= ongoing_event.pom_goal:
            State.goal_reached = True

            await send_embed_message(
                ctx,
                title=ongoing_event.event_name,
                description=(
                    f"We've reached our goal of {ongoing_event.pom_goal} "
                    "poms! Well done and keep up the good work!"),
            )
示例#11
0
 async def reset(self, ctx: Context):
     """Permanently deletes all of your poms. This cannot be undone."""
     Storage.delete_all_user_poms(ctx.author)
     await ctx.message.add_reaction(Reactions.WASTEBASKET)