async def system_fronter(ctx: CommandContext, args: List[str]): if len(args) == 0: if not ctx.system: raise NoSystemRegistered() 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] embed = utils.make_default_embed(None) if len(fronter_names) == 0: embed.add_field(name="Current fronter", value="(no fronter)") elif len(fronter_names) == 1: embed.add_field(name="Current fronter", value=fronter_names[0]) else: embed.add_field(name="Current fronters", value=", ".join(fronter_names)) if timestamp: embed.add_field(name="Since", value="{} ({})".format( timestamp.isoformat(sep=" ", timespec="seconds"), humanize.naturaltime(timestamp))) return embed
async def switch_move(ctx: MemberCommandContext, args: List[str]): if len(args) == 0: return embeds.error("You must pass a time to move the switch to.", help=help.switch_move) # Parse the time to move to new_time = dateparser.parse(" ".join(args), languages=["en"], settings={ "TO_TIMEZONE": "UTC", "RETURN_AS_TIMEZONE_AWARE": False }) if not new_time: return embeds.error("{} can't be parsed as a valid time.".format(" ".join(args))) # Make sure the time isn't in the future if new_time > datetime.now(): return embeds.error("Can't move switch to a time in the future.") # Make sure it all runs in a big transaction for atomicity async with ctx.conn.transaction(): # Get the last two switches to make sure the switch to move isn't before the second-last switch last_two_switches = await pluralkit.utils.get_front_history(ctx.conn, ctx.system.id, count=2) if len(last_two_switches) == 0: return embeds.error("There are no registered switches for this system.") last_timestamp, last_fronters = last_two_switches[0] if len(last_two_switches) > 1: second_last_timestamp, _ = last_two_switches[1] if new_time < second_last_timestamp: time_str = humanize.naturaltime(second_last_timestamp) return embeds.error("Can't move switch to before last switch time ({}), as it would cause conflicts.".format(time_str)) # Display the confirmation message w/ humanized times members = ", ".join([member.name for member in last_fronters]) or "nobody" last_absolute = last_timestamp.isoformat(sep=" ", timespec="seconds") last_relative = humanize.naturaltime(last_timestamp) new_absolute = new_time.isoformat(sep=" ", timespec="seconds") new_relative = humanize.naturaltime(new_time) embed = utils.make_default_embed("This will move the latest switch ({}) from {} ({}) to {} ({}). Is this OK?".format(members, last_absolute, last_relative, new_absolute, new_relative)) # Await and handle confirmation reactions confirm_msg = await ctx.reply(embed=embed) await ctx.client.add_reaction(confirm_msg, "✅") await ctx.client.add_reaction(confirm_msg, "❌") reaction = await ctx.client.wait_for_reaction(emoji=["✅", "❌"], message=confirm_msg, user=ctx.message.author, timeout=60.0) if not reaction: return embeds.error("Switch move timed out.") if reaction.reaction.emoji == "❌": return embeds.error("Switch move cancelled.") # DB requires the actual switch ID which our utility method above doesn't return, do this manually switch_id = (await db.front_history(ctx.conn, ctx.system.id, count=1))[0]["id"] # Change the switch in the DB await db.move_last_switch(ctx.conn, ctx.system.id, switch_id, new_time) return embeds.success("Switch moved.")
async def show_help(ctx: CommandContext, args: List[str]): embed = utils.make_default_embed("") embed.title = "PluralKit Help" embed.set_footer( text= "By Astrid (Ske#6201, or 'qoxvy' on PK) | GitHub: https://github.com/xSke/PluralKit/" ) category = args[0] if len(args) > 0 else None from pluralkit.bot.help import help_pages if category in help_pages: for name, text in help_pages[category]: if name: embed.add_field(name=name, value=text) else: embed.description = text else: return embeds.error("Unknown help page '{}'.".format(category)) return embed
async def system_fronthistory(ctx: CommandContext, args: List[str]): if len(args) == 0: if not ctx.system: raise NoSystemRegistered() 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): # Special case when no one's fronting if len(members) == 0: name = "(no fronter)" else: name = ", ".join([member.name for member in members]) # Make proper date string time_text = timestamp.isoformat(sep=" ", timespec="seconds") rel_text = humanize.naturaltime(timestamp) delta_text = "" if i > 0: last_switch_time = front_history[i - 1][0] delta_text = ", for {}".format( humanize.naturaldelta(timestamp - last_switch_time)) lines.append("**{}** ({}, {}{})".format(name, time_text, rel_text, delta_text)) embed = utils.make_default_embed("\n".join(lines) or "(none)") embed.title = "Past switches" return embed
async def system_set(ctx: CommandContext, args: List[str]): if len(args) == 0: raise InvalidCommandSyntax() allowed_properties = ["name", "description", "tag", "avatar"] db_properties = { "name": "name", "description": "description", "tag": "tag", "avatar": "avatar_url" } prop = args[0] if prop not in allowed_properties: raise CommandError( "Unknown property {}. Allowed properties are {}.".format( prop, ", ".join(allowed_properties))) if len(args) >= 2: value = " ".join(args[1:]) # Sanity checking if prop == "tag": if len(value) > 32: raise CommandError( "Can't have system tag longer than 32 characters.") # Make sure there are no members which would make the combined length exceed 32 members_exceeding = await db.get_members_exceeding( ctx.conn, system_id=ctx.system.id, length=32 - len(value) - 1) if len(members_exceeding) > 0: # If so, error out and warn member_names = ", ".join( [member.name for member in members_exceeding]) logger.debug( "Members exceeding combined length with tag '{}': {}". format(value, member_names)) raise CommandError( "The maximum length of a name plus the system tag is 32 characters. The following members would exceed the limit: {}. Please reduce the length of the tag, or rename the members." .format(member_names)) if prop == "avatar": user = await utils.parse_mention(ctx.client, value) if user: # Set the avatar to the mentioned user's avatar # Discord doesn't like webp, but also hosts png alternatives value = user.avatar_url.replace(".webp", ".png") else: # Validate URL u = urlparse(value) if u.scheme in ["http", "https"] and u.netloc and u.path: value = value else: raise CommandError("Invalid URL.") else: # Clear from DB value = None 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) return response
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