diff --git a/src/pluralkit/bot/commands/system_commands.py b/src/pluralkit/bot/commands/system_commands.py index 91d5b563..ebd0b13d 100644 --- a/src/pluralkit/bot/commands/system_commands.py +++ b/src/pluralkit/bot/commands/system_commands.py @@ -1,11 +1,11 @@ -import logging +from datetime import datetime from typing import List from urllib.parse import urlparse +import dateparser import humanize import pluralkit.utils -from pluralkit.bot import utils from pluralkit.bot.commands import * logger = logging.getLogger("pluralkit.commands") @@ -22,7 +22,7 @@ async def system_info(ctx: CommandContext, args: List[str]): if system is None: raise CommandError("Unable to find system \"{}\".".format(args[0])) - + await ctx.reply(embed=await utils.generate_system_info_card(ctx.conn, ctx.client, system)) @command(cmd="system new", usage="[name]", description="Registers a new system to this account.", category="System commands", system_required=False) @@ -46,7 +46,7 @@ async def new_system(ctx: CommandContext, args: List[str]): @command(cmd="system set", usage=" [value]", description="Edits a system property. Leave [value] blank to clear.", category="System commands") async def system_set(ctx: CommandContext, args: List[str]): - if len(args) == 0: + if len(args) == 0: raise InvalidCommandSyntax() allowed_properties = ["name", "description", "tag", "avatar"] @@ -96,7 +96,7 @@ async def system_set(ctx: CommandContext, args: List[str]): db_prop = db_properties[prop] await db.update_system_field(ctx.conn, system_id=ctx.system.id, field=db_prop, value=value) - + response = utils.make_default_embed("{} system {}.".format("Updated" if value else "Cleared", prop)) if prop == "avatar" and value: response.set_image(url=value) @@ -150,10 +150,10 @@ async def system_fronter(ctx: CommandContext, args: List[str]): system = ctx.system else: system = await utils.get_system_fuzzy(ctx.conn, ctx.client, args[0]) - + if system is None: raise CommandError("Can't find system \"{}\".".format(args[0])) - + fronters, timestamp = await pluralkit.utils.get_fronters(ctx.conn, system_id=system.id) fronter_names = [member.name for member in fronters] @@ -178,10 +178,10 @@ async def system_fronthistory(ctx: CommandContext, args: List[str]): system = ctx.system else: system = await utils.get_system_fuzzy(ctx.conn, ctx.client, args[0]) - + if system is None: raise CommandError("Can't find system \"{}\".".format(args[0])) - + lines = [] front_history = await pluralkit.utils.get_front_history(ctx.conn, system.id, count=10) for i, (timestamp, members) in enumerate(front_history): @@ -215,4 +215,85 @@ async def system_delete(ctx: CommandContext, args: List[str]): await db.remove_system(ctx.conn, system_id=ctx.system.id) return "System deleted." else: - return "System deletion cancelled." \ No newline at end of file + return "System deletion cancelled." + + +@command(cmd="system frontpercent", usage="[time]", + description="Shows the fronting percentage of every member, averaged over the given time", + category="System commands") +async def system_frontpercent(ctx: CommandContext, args: List[str]): + # Parse the time limit (will go this far back) + before = dateparser.parse(" ".join(args), languages=["en"], settings={ + "TO_TIMEZONE": "UTC", + "RETURN_AS_TIMEZONE_AWARE": False + }) + + # If time is in the future, just kinda discard + if before and before > datetime.utcnow(): + before = None + + # Fetch list of switches + all_switches = await pluralkit.utils.get_front_history(ctx.conn, ctx.system.id, 99999) + if not all_switches: + raise CommandError("No switches registered to this system.") + + # Cull the switches *ending* before the limit, if given + # We'll need to find the first switch starting before the limit, then cut off every switch *before* that + if before: + for last_stamp, _ in all_switches: + if last_stamp < before: + break + + all_switches = [(stamp, members) for stamp, members in all_switches if stamp >= last_stamp] + + start_times = [stamp for stamp, _ in all_switches] + end_times = [datetime.utcnow()] + start_times + switch_members = [members for _, members in all_switches] + + # Gonna save a list of members by ID for future lookup too + members_by_id = {} + + # Using the ID as a key here because it's a simple number that can be hashed and used as a key + member_times = {} + for start_time, end_time, members in zip(start_times, end_times, switch_members): + # Cut off parts of the switch that occurs before the time limit (will only happen if this is the last switch) + if before and start_time < before: + start_time = before + + # Calculate length of the switch + switch_length = end_time - start_time + + def add_switch(member_id, length): + if member_id not in member_times: + member_times[member_id] = length + else: + member_times[member_id] += length + + for member in members: + # Add the switch length to the currently registered time for that member + add_switch(member.id, switch_length) + + # Also save the member in the ID map for future reference + members_by_id[member.id] = member + + # Also register a no-fronter switch with the key None + if not members: + add_switch(None, switch_length) + + # Find the total timespan of the range + span_start = max(start_times[-1], before) if before else start_times[-1] + total_time = datetime.utcnow() - span_start + + embed = utils.make_default_embed(None) + for member_id, front_time in sorted(member_times.items(), key=lambda x: x[1], reverse=True): + member = members_by_id[member_id] if member_id else None + + # Calculate percent + fraction = front_time / total_time + percent = int(fraction * 100) + + embed.add_field(name=member.name if member else "(no fronter)", + value="{}% ({})".format(percent, humanize.naturaldelta(front_time))) + + embed.set_footer(text="Since {}".format(span_start.isoformat(sep=" ", timespec="seconds"))) + return embed diff --git a/src/pluralkit/bot/help.py b/src/pluralkit/bot/help.py index 7de93edc..8ce6bffe 100644 --- a/src/pluralkit/bot/help.py +++ b/src/pluralkit/bot/help.py @@ -137,7 +137,16 @@ For example: `pk;system fronter` - Shows the current fronter(s) in your own system. `pk;system fronter abcde` - Shows the current fronter in the system with the ID `abcde`. `pk;system fronthistory` - Shows the past 10 switches in your own system. -`pk;system fronthistory @JohnsAccount` - Shows the past 10 switches in the system linked to @JohnsAccount.""") +`pk;system fronthistory @JohnsAccount` - Shows the past 10 switches in the system linked to @JohnsAccount."""), + ("Viewing a front breakdown", + """To see a per-member breakdown of your switches, use the `pk;system frontpercent` command. You can optionally give it a time limit to only count switches after that point. + +For example: +`pk;system frontpercent` - Shows a front breakdown for your system since you started logging switches +`pk;system frontpercent 1 day` - Shows a front breakdown for your system for the past day +`pk;system frontpercent Jan 1st 2018` - Shows a front breakdown for your system since January 1st, 2018 + +Note that the percentages don't necessarily add up to 100%, as multiple members can be listed as fronting at a time.""") ], "mod": [ (None, "Note that all moderation commands require you to have administrator privileges on the server they're used on."),