255 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			255 lines
		
	
	
		
			9.3 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import dateparser
 | |
| import humanize
 | |
| from datetime import datetime, timedelta
 | |
| 
 | |
| import pluralkit.utils
 | |
| from pluralkit.bot import help
 | |
| from pluralkit.bot.commands import *
 | |
| from pluralkit.errors import ExistingSystemError, UnlinkingLastAccountError, PluralKitError, AccountAlreadyLinkedError
 | |
| 
 | |
| logger = logging.getLogger("pluralkit.commands")
 | |
| 
 | |
| 
 | |
| async def system_info(ctx: CommandContext):
 | |
|     if ctx.has_next():
 | |
|         system = await ctx.pop_system()
 | |
|     else:
 | |
|         system = await ctx.ensure_system()
 | |
| 
 | |
|     await ctx.reply(embed=await utils.generate_system_info_card(ctx.conn, ctx.client, system))
 | |
| 
 | |
| 
 | |
| async def new_system(ctx: CommandContext):
 | |
|     system_name = ctx.remaining() or None
 | |
| 
 | |
|     try:
 | |
|         await System.create_system(ctx.conn, ctx.message.author.id, system_name)
 | |
|     except ExistingSystemError as e:
 | |
|         return CommandError(e.message)
 | |
| 
 | |
|     return CommandSuccess("System registered! To begin adding members, use `pk;member new <name>`.")
 | |
| 
 | |
| 
 | |
| async def system_set(ctx: CommandContext):
 | |
|     system = await ctx.ensure_system()
 | |
| 
 | |
|     property_name = ctx.pop_str(CommandError("You must pass a property name to set.", help=help.edit_system))
 | |
| 
 | |
|     async def avatar_setter(conn, url):
 | |
|         if url:
 | |
|             user = await utils.parse_mention(ctx.client, url)
 | |
|             if user:
 | |
|                 # Set the avatar to the mentioned user's avatar
 | |
|                 # Discord pushes webp by default, which isn't supported by webhooks, but also hosts png alternatives
 | |
|                 url = user.avatar_url.replace(".webp", ".png")
 | |
| 
 | |
|             await system.set_avatar(conn, url)
 | |
| 
 | |
|     properties = {
 | |
|         "name": system.set_name,
 | |
|         "description": system.set_description,
 | |
|         "tag": system.set_tag,
 | |
|         "avatar": avatar_setter
 | |
|     }
 | |
| 
 | |
|     if property_name not in properties:
 | |
|         return CommandError(
 | |
|             "Unknown property {}. Allowed properties are {}.".format(property_name, ", ".join(properties.keys())),
 | |
|             help=help.edit_system)
 | |
| 
 | |
|     value = ctx.remaining() or None
 | |
| 
 | |
|     try:
 | |
|         await properties[property_name](ctx.conn, value)
 | |
|     except PluralKitError as e:
 | |
|         return CommandError(e.message)
 | |
| 
 | |
|     response = CommandSuccess("{} system {}.".format("Updated" if value else "Cleared", property_name))
 | |
|     # if prop == "avatar" and value:
 | |
|     #    response.set_image(url=value)
 | |
|     return response
 | |
| 
 | |
| 
 | |
| async def system_link(ctx: CommandContext):
 | |
|     system = await ctx.ensure_system()
 | |
|     account_name = ctx.pop_str(CommandError("You must pass an account to link this system to.", help=help.link_account))
 | |
| 
 | |
|     # Find account to link
 | |
|     linkee = await utils.parse_mention(ctx.client, account_name)
 | |
|     if not linkee:
 | |
|         return CommandError("Account not found.")
 | |
| 
 | |
|     # Make sure account doesn't already have a system
 | |
|     account_system = await System.get_by_account(ctx.conn, linkee.id)
 | |
|     if account_system:
 | |
|         return CommandError(AccountAlreadyLinkedError(account_system).message)
 | |
| 
 | |
|     if not await ctx.confirm_react(linkee, "{}, please confirm the link by clicking the ✅ reaction on this message.".format(linkee.mention)):
 | |
|         return CommandError("Account link cancelled.")
 | |
| 
 | |
|     await system.link_account(ctx.conn, linkee.id)
 | |
|     return CommandSuccess("Account linked to system.")
 | |
| 
 | |
| 
 | |
| async def system_unlink(ctx: CommandContext):
 | |
|     system = await ctx.ensure_system()
 | |
| 
 | |
|     try:
 | |
|         await system.unlink_account(ctx.conn, ctx.message.author.id)
 | |
|     except UnlinkingLastAccountError as e:
 | |
|         return CommandError(e.message)
 | |
| 
 | |
|     return CommandSuccess("Account unlinked.")
 | |
| 
 | |
| 
 | |
| async def system_fronter(ctx: CommandContext):
 | |
|     if ctx.has_next():
 | |
|         system = await ctx.pop_system()
 | |
|     else:
 | |
|         system = await ctx.ensure_system()
 | |
| 
 | |
|     fronters, timestamp = await pluralkit.utils.get_fronters(ctx.conn, system_id=system.id)
 | |
|     fronter_names = [member.name for member in fronters]
 | |
| 
 | |
|     embed = embeds.status("")
 | |
| 
 | |
|     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(pluralkit.utils.fix_time(timestamp))))
 | |
|     await ctx.reply(embed=embed)
 | |
| 
 | |
| 
 | |
| async def system_fronthistory(ctx: CommandContext):
 | |
|     if ctx.has_next():
 | |
|         system = await ctx.pop_system()
 | |
|     else:
 | |
|         system = await ctx.ensure_system()
 | |
| 
 | |
|     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(pluralkit.utils.fix_time(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 = embeds.status("\n".join(lines) or "(none)")
 | |
|     embed.title = "Past switches"
 | |
|     await ctx.reply(embed=embed)
 | |
| 
 | |
| 
 | |
| async def system_delete(ctx: CommandContext):
 | |
|     system = await ctx.ensure_system()
 | |
| 
 | |
|     delete_confirm_msg = "Are you sure you want to delete your system? If so, reply to this message with the system's ID (`{}`).".format(
 | |
|         system.hid)
 | |
|     if not await ctx.confirm_text(ctx.message.author, ctx.message.channel, system.hid, delete_confirm_msg):
 | |
|         return CommandError("System deletion cancelled.")
 | |
| 
 | |
|     await system.delete(ctx.conn)
 | |
|     return CommandSuccess("System deleted.")
 | |
| 
 | |
| 
 | |
| async def system_frontpercent(ctx: CommandContext):
 | |
|     system = await ctx.ensure_system()
 | |
| 
 | |
|     # Parse the time limit (will go this far back)
 | |
|     if ctx.remaining():
 | |
|         before = dateparser.parse(ctx.remaining(), languages=["en"], settings={
 | |
|             "TO_TIMEZONE": "UTC",
 | |
|             "RETURN_AS_TIMEZONE_AWARE": False
 | |
|         })
 | |
| 
 | |
|         if not before:
 | |
|             return CommandError("Could not parse '{}' as a valid time.".format(ctx.remaining()))
 | |
| 
 | |
|         # If time is in the future, just kinda discard
 | |
|         if before and before > datetime.utcnow():
 | |
|             before = None
 | |
|     else:
 | |
|         before = datetime.utcnow() - timedelta(days=30)
 | |
| 
 | |
|     # Fetch list of switches
 | |
|     all_switches = await pluralkit.utils.get_front_history(ctx.conn, system.id, 99999)
 | |
|     if not all_switches:
 | |
|         return 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(id, length):
 | |
|             if id not in member_times:
 | |
|                 member_times[id] = length
 | |
|             else:
 | |
|                 member_times[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 = embeds.status("")
 | |
|     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 = round(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"), humanize.naturaltime(pluralkit.utils.fix_time(span_start))))
 | |
|     await ctx.reply(embed=embed)
 |