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)
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)
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)
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:", )
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}")
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)
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*"} """), )
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", ))
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")}* """), )
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!"), )
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)