async def switch_member(ctx: MemberCommandContext, args: List[str]): if len(args) == 0: return embeds.error("You must pass at least one member name or ID to register a switch to.", help=help.switch_register) members: List[Member] = [] for member_name in args: # Find the member member = await utils.get_member_fuzzy(ctx.conn, ctx.system.id, member_name) if not member: return embeds.error("Couldn't find member \"{}\".".format(member_name)) members.append(member) # Compare requested switch IDs and existing fronter IDs to check for existing switches # Lists, because order matters, it makes sense to just swap fronters member_ids = [member.id for member in members] fronter_ids = (await pluralkit.utils.get_fronter_ids(ctx.conn, ctx.system.id))[0] if member_ids == fronter_ids: if len(members) == 1: return embeds.error("{} is already fronting.".format(members[0].name)) return embeds.error("Members {} are already fronting.".format(", ".join([m.name for m in members]))) # Also make sure there aren't any duplicates if len(set(member_ids)) != len(member_ids): return embeds.error("Duplicate members in member list.") # Log the switch async with ctx.conn.transaction(): switch_id = await db.add_switch(ctx.conn, system_id=ctx.system.id) for member in members: await db.add_switch_member(ctx.conn, switch_id=switch_id, member_id=member.id) if len(members) == 1: return embeds.success("Switch registered. Current fronter is now {}.".format(members[0].name)) else: return embeds.success("Switch registered. Current fronters are now {}.".format(", ".join([m.name for m in members])))
async def new_system(ctx: CommandContext, args: List[str]): if ctx.system: return embeds.error( "You already have a system registered. To delete your system, use `pk;system delete`, or to unlink your system from this account, use `pk;system unlink`." ) system_name = None if len(args) > 0: system_name = " ".join(args) async with ctx.conn.transaction(): # TODO: figure out what to do if this errors out on collision on generate_hid hid = utils.generate_hid() system = await db.create_system(ctx.conn, system_name=system_name, system_hid=hid) # Link account await db.link_account(ctx.conn, system_id=system.id, account_id=ctx.message.author.id) return embeds.success( "System registered! To begin adding members, use `pk;member new <name>`." )
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 switch_out(ctx: MemberCommandContext, args: List[str]): # Get current fronters fronters, _ = await pluralkit.utils.get_fronter_ids(ctx.conn, system_id=ctx.system.id) if not fronters: return embeds.error("There's already no one in front.") # Log it, and don't log any members await db.add_switch(ctx.conn, system_id=ctx.system.id) return embeds.success("Switch-out registered.")
async def system_unlink(ctx: CommandContext, args: List[str]): # Make sure you can't unlink every account linked_accounts = await db.get_linked_accounts(ctx.conn, system_id=ctx.system.id) if len(linked_accounts) == 1: return embeds.error( "This is the only account on your system, so you can't unlink it.") await db.unlink_account(ctx.conn, system_id=ctx.system.id, account_id=ctx.message.author.id) return embeds.success("Account unlinked.")
async def system_delete(ctx: CommandContext, args: List[str]): await ctx.reply( "Are you sure you want to delete your system? If so, reply to this message with the system's ID (`{}`)." .format(ctx.system.hid)) msg = await ctx.client.wait_for_message(author=ctx.message.author, channel=ctx.message.channel, timeout=60.0) if msg and msg.content.lower() == ctx.system.hid.lower(): await db.remove_system(ctx.conn, system_id=ctx.system.id) return embeds.success("System deleted.") else: return embeds.error("System deletion cancelled.")
async def invite_link(ctx: CommandContext, args: List[str]): client_id = os.environ["CLIENT_ID"] permissions = discord.Permissions() permissions.manage_webhooks = True permissions.send_messages = True permissions.manage_messages = True permissions.embed_links = True permissions.attach_files = True permissions.read_message_history = True permissions.add_reactions = True url = oauth_url(client_id, permissions) logger.debug("Sending invite URL: {}".format(url)) return embeds.success( "Use this link to add PluralKit to your server: {}".format(url))
async def set_log(ctx: CommandContext, args: List[str]): if not ctx.message.author.server_permissions.administrator: return embeds.error( "You must be a server administrator to use this command.") server = ctx.message.server if len(args) == 0: channel_id = None else: channel = utils.parse_channel_mention(args[0], server=server) if not channel: return embeds.error("Channel not found.") channel_id = channel.id await db.update_server(ctx.conn, server.id, logging_channel_id=channel_id) return embeds.success("Updated logging channel." if channel_id else "Cleared logging channel.")
async def system_link(ctx: CommandContext, args: List[str]): if len(args) == 0: return embeds.error("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, args[0]) if not linkee: return embeds.error("Account not found.") # Make sure account doesn't already have a system account_system = await db.get_system_by_account(ctx.conn, linkee.id) if account_system: return embeds.error( "The mentioned account is already linked to a system (`{}`)". format(account_system.hid)) # Send confirmation message msg = await ctx.reply( "{}, please confirm the link by clicking the ✅ reaction on this message." .format(linkee.mention)) await ctx.client.add_reaction(msg, "✅") await ctx.client.add_reaction(msg, "❌") reaction = await ctx.client.wait_for_reaction(emoji=["✅", "❌"], message=msg, user=linkee, timeout=60.0) # If account to be linked confirms... if not reaction: return embeds.error("Account link timed out.") if not reaction.reaction.emoji == "✅": return embeds.error("Account link cancelled.") await db.link_account(ctx.conn, system_id=ctx.system.id, account_id=linkee.id) return embeds.success("Account linked to system.")
async def system_set(ctx: CommandContext, args: List[str]): if len(args) == 0: return embeds.error("You must pass a property name to set.", help=help.edit_system) 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 embeds.error( "Unknown property {}. Allowed properties are {}.".format( prop, ", ".join(allowed_properties)), help=help.edit_system) if len(args) >= 2: value = " ".join(args[1:]) # Sanity checking if prop == "tag": if len(value) > 32: raise embeds.error( "You can't have a 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 embeds.error( "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 embeds.error("Invalid image 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 = embeds.success("{} system {}.".format( "Updated" if value else "Cleared", prop)) if prop == "avatar" and value: response.set_image(url=value) return response
def to_embed(self): return embeds.success("\u2705 " + self.text)