System/member set command rework, should be more user friendly now
This commit is contained in:
		| @@ -6,6 +6,7 @@ from typing import Tuple, Optional, Union | |||||||
|  |  | ||||||
| from pluralkit import db | from pluralkit import db | ||||||
| from pluralkit.bot import embeds, utils | from pluralkit.bot import embeds, utils | ||||||
|  | from pluralkit.errors import PluralKitError | ||||||
| from pluralkit.member import Member | from pluralkit.member import Member | ||||||
| from pluralkit.system import System | from pluralkit.system import System | ||||||
|  |  | ||||||
| @@ -13,6 +14,7 @@ logger = logging.getLogger("pluralkit.bot.commands") | |||||||
|  |  | ||||||
|  |  | ||||||
| def next_arg(arg_string: str) -> Tuple[str, Optional[str]]: | def next_arg(arg_string: str) -> Tuple[str, Optional[str]]: | ||||||
|  |     # A basic quoted-arg parser | ||||||
|     if arg_string.startswith("\""): |     if arg_string.startswith("\""): | ||||||
|         end_quote = arg_string[1:].find("\"") + 1 |         end_quote = arg_string[1:].find("\"") + 1 | ||||||
|         if end_quote > 0: |         if end_quote > 0: | ||||||
| @@ -135,18 +137,27 @@ import pluralkit.bot.commands.system_commands | |||||||
|  |  | ||||||
|  |  | ||||||
| async def run_command(ctx: CommandContext, func): | async def run_command(ctx: CommandContext, func): | ||||||
|  |     # lol nested try | ||||||
|     try: |     try: | ||||||
|         result = await func(ctx) |         try: | ||||||
|  |             await func(ctx) | ||||||
|  |         except PluralKitError as e: | ||||||
|  |             raise CommandError(e.message, e.help_page) | ||||||
|     except CommandError as e: |     except CommandError as e: | ||||||
|         content, embed = e.format() |         content, embed = e.format() | ||||||
|         await ctx.reply(content=content, embed=embed) |         await ctx.reply(content=content, embed=embed) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def command_dispatch(client: discord.Client, message: discord.Message, conn) -> bool: | async def command_dispatch(client: discord.Client, message: discord.Message, conn) -> bool: | ||||||
|     prefix = "^(pk(;|!)|<@{}> )".format(client.user.id) |     prefix = "^(pk(;|!)|<@{}> )".format(client.user.id) | ||||||
|     commands = [ |     commands = [ | ||||||
|         (r"system (new|register|create|init)", system_commands.new_system), |         (r"system (new|register|create|init)", system_commands.new_system), | ||||||
|         (r"system set", system_commands.system_set), |         (r"system set", system_commands.system_set), | ||||||
|  |         (r"system (name|rename)", system_commands.system_name), | ||||||
|  |         (r"system description", system_commands.system_description), | ||||||
|  |         (r"system avatar", system_commands.system_avatar), | ||||||
|  |         (r"system tag", system_commands.system_tag), | ||||||
|         (r"system link", system_commands.system_link), |         (r"system link", system_commands.system_link), | ||||||
|         (r"system unlink", system_commands.system_unlink), |         (r"system unlink", system_commands.system_unlink), | ||||||
|         (r"system fronter", system_commands.system_fronter), |         (r"system fronter", system_commands.system_fronter), | ||||||
| @@ -159,6 +170,12 @@ async def command_dispatch(client: discord.Client, message: discord.Message, con | |||||||
|  |  | ||||||
|         (r"member (new|create|add|register)", member_commands.new_member), |         (r"member (new|create|add|register)", member_commands.new_member), | ||||||
|         (r"member set", member_commands.member_set), |         (r"member set", member_commands.member_set), | ||||||
|  |         (r"member (name|rename)", member_commands.member_name), | ||||||
|  |         (r"member description", member_commands.member_description), | ||||||
|  |         (r"member avatar", member_commands.member_avatar), | ||||||
|  |         (r"member color", member_commands.member_color), | ||||||
|  |         (r"member (pronouns|pronoun)", member_commands.member_pronouns), | ||||||
|  |         (r"member (birthday|birthdate)", member_commands.member_birthdate), | ||||||
|         (r"member proxy", member_commands.member_proxy), |         (r"member proxy", member_commands.member_proxy), | ||||||
|         (r"member (delete|remove|destroy|erase)", member_commands.member_delete), |         (r"member (delete|remove|destroy|erase)", member_commands.member_delete), | ||||||
|         (r"member", member_commands.member_info), |         (r"member", member_commands.member_info), | ||||||
|   | |||||||
| @@ -1,5 +1,3 @@ | |||||||
| from datetime import datetime |  | ||||||
|  |  | ||||||
| import pluralkit.bot.embeds | import pluralkit.bot.embeds | ||||||
| from pluralkit.bot import help | from pluralkit.bot import help | ||||||
| from pluralkit.bot.commands import * | from pluralkit.bot.commands import * | ||||||
| @@ -27,73 +25,72 @@ async def new_member(ctx: CommandContext): | |||||||
|         raise CommandError(e.message) |         raise CommandError(e.message) | ||||||
|  |  | ||||||
|     await ctx.reply_ok( |     await ctx.reply_ok( | ||||||
|         "Member \"{}\" (`{}`) registered! To register their proxy tags, use `pk;member proxy`.".format(new_name, member.hid)) |         "Member \"{}\" (`{}`) registered! To register their proxy tags, use `pk;member proxy`.".format(new_name, | ||||||
|  |                                                                                                        member.hid)) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def member_set(ctx: CommandContext): | async def member_set(ctx: CommandContext): | ||||||
|  |     raise CommandError( | ||||||
|  |         "`pk;member set` has been retired. Please use the new member modifying commands: `pk;member [name|description|avatar|color|pronouns|birthdate]`.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def member_name(ctx: CommandContext): | ||||||
|     system = await ctx.ensure_system() |     system = await ctx.ensure_system() | ||||||
|     member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member)) |     member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member)) | ||||||
|  |     new_name = ctx.pop_str(CommandError("You must pass a new member name.", help=help.edit_member)) | ||||||
|  |  | ||||||
|     property_name = ctx.pop_str(CommandError("You must pass a property name to set.", help=help.edit_member)) |     await member.set_name(ctx.conn, system, new_name) | ||||||
|  |     await ctx.reply_ok("Member name updated.") | ||||||
|  |  | ||||||
|     async def name_setter(conn, new_name): |  | ||||||
|         if not new_name: |  | ||||||
|             raise CommandError("You can't clear the member name.") |  | ||||||
|         await member.set_name(conn, system, new_name) |  | ||||||
|  |  | ||||||
|     async def avatar_setter(conn, url): | async def member_description(ctx: CommandContext): | ||||||
|         if url: |     await ctx.ensure_system() | ||||||
|             user = await utils.parse_mention(ctx.client, url) |     member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member)) | ||||||
|             if user: |     new_description = ctx.remaining() or None | ||||||
|                 # 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 member.set_avatar(conn, url) |     await member.set_description(ctx.conn, new_description) | ||||||
|  |     await ctx.reply_ok("Member description {}.".format("updated" if new_description else "cleared")) | ||||||
|  |  | ||||||
|     async def birthdate_setter(conn, date_str): |  | ||||||
|         if date_str: |  | ||||||
|             try: |  | ||||||
|                 date = datetime.strptime(date_str, "%Y-%m-%d").date() |  | ||||||
|             except ValueError: |  | ||||||
|                 try: |  | ||||||
|                     # Try again, adding 0001 as a placeholder year |  | ||||||
|                     # This is considered a "null year" and will be omitted from the info card |  | ||||||
|                     # Useful if you want your birthday to be displayed yearless. |  | ||||||
|                     date = datetime.strptime("0001-" + date_str, "%Y-%m-%d").date() |  | ||||||
|                 except ValueError: |  | ||||||
|                     raise CommandError("Invalid date. Date must be in ISO-8601 format (YYYY-MM-DD, eg. 1999-07-25).") |  | ||||||
|         else: |  | ||||||
|             date = None |  | ||||||
|  |  | ||||||
|         await member.set_birthdate(conn, date) | async def member_avatar(ctx: CommandContext): | ||||||
|  |     await ctx.ensure_system() | ||||||
|  |     member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member)) | ||||||
|  |     new_avatar_url = ctx.remaining() or None | ||||||
|  |  | ||||||
|     properties = { |     if new_avatar_url: | ||||||
|         "name": name_setter, |         user = await utils.parse_mention(ctx.client, new_avatar_url) | ||||||
|         "description": member.set_description, |         if user: | ||||||
|         "avatar": avatar_setter, |             new_avatar_url = user.avatar_url_as(format="png") | ||||||
|         "color": member.set_color, |  | ||||||
|         "pronouns": member.set_pronouns, |  | ||||||
|         "birthdate": birthdate_setter, |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if property_name not in properties: |     await member.set_avatar(ctx.conn, new_avatar_url) | ||||||
|         raise CommandError( |     await ctx.reply_ok("Member avatar {}.".format("updated" if new_avatar_url else "cleared")) | ||||||
|             "Unknown property {}. Allowed properties are {}.".format(property_name, ", ".join(properties.keys())), |  | ||||||
|             help=help.edit_system) |  | ||||||
|  |  | ||||||
|     value = ctx.remaining() or None |  | ||||||
|  |  | ||||||
|     try: | async def member_color(ctx: CommandContext): | ||||||
|         await properties[property_name](ctx.conn, value) |     await ctx.ensure_system() | ||||||
|     except PluralKitError as e: |     member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member)) | ||||||
|         raise CommandError(e.message) |     new_color = ctx.remaining() or None | ||||||
|  |  | ||||||
|     # if prop == "avatar" and value: |     await member.set_color(ctx.conn, new_color) | ||||||
|     #    response.set_image(url=value) |     await ctx.reply_ok("Member color {}.".format("updated" if new_color else "cleared")) | ||||||
|     # if prop == "color" and value: |  | ||||||
|     #    response.colour = int(value, 16) |  | ||||||
|     await ctx.reply_ok("{} member {}.".format("Updated" if value else "Cleared", property_name)) | async def member_pronouns(ctx: CommandContext): | ||||||
|  |     await ctx.ensure_system() | ||||||
|  |     member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member)) | ||||||
|  |     new_pronouns = ctx.remaining() or None | ||||||
|  |  | ||||||
|  |     await member.set_pronouns(ctx.conn, new_pronouns) | ||||||
|  |     await ctx.reply_ok("Member pronouns {}.".format("updated" if new_pronouns else "cleared")) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def member_birthdate(ctx: CommandContext): | ||||||
|  |     await ctx.ensure_system() | ||||||
|  |     member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.edit_member)) | ||||||
|  |     new_birthdate = ctx.remaining() or None | ||||||
|  |  | ||||||
|  |     await member.set_birthdate(ctx.conn, new_birthdate) | ||||||
|  |     await ctx.reply_ok("Member birthdate {}.".format("updated" if new_birthdate else "cleared")) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def member_proxy(ctx: CommandContext): | async def member_proxy(ctx: CommandContext): | ||||||
| @@ -110,7 +107,7 @@ async def member_proxy(ctx: CommandContext): | |||||||
|  |  | ||||||
|         if example.count("text") != 1: |         if example.count("text") != 1: | ||||||
|             raise CommandError("Example proxy message must contain the string 'text' exactly once.", |             raise CommandError("Example proxy message must contain the string 'text' exactly once.", | ||||||
|                                 help=help.member_proxy) |                                help=help.member_proxy) | ||||||
|  |  | ||||||
|         # Extract prefix and suffix |         # Extract prefix and suffix | ||||||
|         prefix = example[:example.index("text")].strip() |         prefix = example[:example.index("text")].strip() | ||||||
| @@ -132,7 +129,8 @@ async def member_delete(ctx: CommandContext): | |||||||
|     await ctx.ensure_system() |     await ctx.ensure_system() | ||||||
|     member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.remove_member)) |     member = await ctx.pop_member(CommandError("You must pass a member name.", help=help.remove_member)) | ||||||
|  |  | ||||||
|     delete_confirm_msg = "Are you sure you want to delete {}? If so, reply to this message with the member's ID (`{}`).".format(member.name, member.hid) |     delete_confirm_msg = "Are you sure you want to delete {}? If so, reply to this message with the member's ID (`{}`).".format( | ||||||
|  |         member.name, member.hid) | ||||||
|     if not await ctx.confirm_text(ctx.message.author, ctx.message.channel, member.hid, delete_confirm_msg): |     if not await ctx.confirm_text(ctx.message.author, ctx.message.channel, member.hid, delete_confirm_msg): | ||||||
|         raise CommandError("Member deletion cancelled.") |         raise CommandError("Member deletion cancelled.") | ||||||
|  |  | ||||||
|   | |||||||
| @@ -32,48 +32,53 @@ async def new_system(ctx: CommandContext): | |||||||
|  |  | ||||||
|  |  | ||||||
| async def system_set(ctx: CommandContext): | async def system_set(ctx: CommandContext): | ||||||
|  |     raise CommandError("`pk;system set` has been retired. Please use the new member modifying commands: `pk;system [name|description|avatar|tag]`.") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def system_name(ctx: CommandContext): | ||||||
|     system = await ctx.ensure_system() |     system = await ctx.ensure_system() | ||||||
|  |     new_name = ctx.remaining() or None | ||||||
|  |  | ||||||
|     property_name = ctx.pop_str(CommandError("You must pass a property name to set.", help=help.edit_system)) |     await system.set_name(ctx.conn, new_name) | ||||||
|  |     await ctx.reply_ok("System name {}.".format("updated" if new_name else "cleared")) | ||||||
|  |  | ||||||
|     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) | async def system_description(ctx: CommandContext): | ||||||
|  |     system = await ctx.ensure_system() | ||||||
|  |     new_description = ctx.remaining() or None | ||||||
|  |  | ||||||
|     properties = { |     await system.set_description(ctx.conn, new_description) | ||||||
|         "name": system.set_name, |     await ctx.reply_ok("System description {}.".format("updated" if new_description else "cleared")) | ||||||
|         "description": system.set_description, |  | ||||||
|         "tag": system.set_tag, |  | ||||||
|         "avatar": avatar_setter |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if property_name not in properties: |  | ||||||
|         raise CommandError( |  | ||||||
|             "Unknown property {}. Allowed properties are {}.".format(property_name, ", ".join(properties.keys())), |  | ||||||
|             help=help.edit_system) |  | ||||||
|  |  | ||||||
|     value = ctx.remaining() or None | async def system_tag(ctx: CommandContext): | ||||||
|  |     system = await ctx.ensure_system() | ||||||
|  |     new_tag = ctx.remaining() or None | ||||||
|  |  | ||||||
|     try: |     await system.set_tag(ctx.conn, new_tag) | ||||||
|         await properties[property_name](ctx.conn, value) |     await ctx.reply_ok("System tag {}.".format("updated" if new_tag else "cleared")) | ||||||
|     except PluralKitError as e: |  | ||||||
|         raise CommandError(e.message) |  | ||||||
|  |  | ||||||
|     await ctx.reply_ok("{} system {}.".format("Updated" if value else "Cleared", property_name)) |  | ||||||
|     # if prop == "avatar" and value: | async def system_avatar(ctx: CommandContext): | ||||||
|     #    response.set_image(url=value) |     system = await ctx.ensure_system() | ||||||
|  |     new_avatar_url = ctx.remaining() or None | ||||||
|  |  | ||||||
|  |     if new_avatar_url: | ||||||
|  |         user = await utils.parse_mention(ctx.client, new_avatar_url) | ||||||
|  |         if user: | ||||||
|  |             new_avatar_url = user.avatar_url_as(format="png") | ||||||
|  |  | ||||||
|  |     await system.set_avatar(ctx.conn, new_avatar_url) | ||||||
|  |     await ctx.reply_ok("System avatar {}.".format("updated" if new_avatar_url else "cleared")) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def system_link(ctx: CommandContext): | async def system_link(ctx: CommandContext): | ||||||
|     system = await ctx.ensure_system() |     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)) |     account_name = ctx.pop_str(CommandError("You must pass an account to link this system to.", help=help.link_account)) | ||||||
|  |  | ||||||
|  |     # Do the sanity checking here too (despite it being done in System.link_account) | ||||||
|  |     # Because we want it to be done before the confirmation dialog is shown | ||||||
|  |  | ||||||
|     # Find account to link |     # Find account to link | ||||||
|     linkee = await utils.parse_mention(ctx.client, account_name) |     linkee = await utils.parse_mention(ctx.client, account_name) | ||||||
|     if not linkee: |     if not linkee: | ||||||
|   | |||||||
| @@ -11,29 +11,41 @@ from pluralkit.switch import Switch | |||||||
| from pluralkit.system import System | from pluralkit.system import System | ||||||
| from pluralkit.utils import get_fronters | from pluralkit.utils import get_fronters | ||||||
|  |  | ||||||
|  | def truncate_field_name(s: str) -> str: | ||||||
|  |     return s[:256] | ||||||
|  |  | ||||||
|  | def truncate_field_body(s: str) -> str: | ||||||
|  |     return s[:1024] | ||||||
|  |  | ||||||
|  | def truncate_description(s: str) -> str: | ||||||
|  |     return s[:2048] | ||||||
|  |  | ||||||
|  | def truncate_title(s: str) -> str: | ||||||
|  |     return s[:256] | ||||||
|  |  | ||||||
|  |  | ||||||
| def success(text: str) -> discord.Embed: | def success(text: str) -> discord.Embed: | ||||||
|     embed = discord.Embed() |     embed = discord.Embed() | ||||||
|     embed.description = text |     embed.description = truncate_description(text) | ||||||
|     embed.colour = discord.Colour.green() |     embed.colour = discord.Colour.green() | ||||||
|     return embed |     return embed | ||||||
|  |  | ||||||
|  |  | ||||||
| def error(text: str, help: Tuple[str, str] = None) -> discord.Embed: | def error(text: str, help: Tuple[str, str] = None) -> discord.Embed: | ||||||
|     embed = discord.Embed() |     embed = discord.Embed() | ||||||
|     embed.description = text |     embed.description = truncate_description(s) | ||||||
|     embed.colour = discord.Colour.dark_red() |     embed.colour = discord.Colour.dark_red() | ||||||
|  |  | ||||||
|     if help: |     if help: | ||||||
|         help_title, help_text = help |         help_title, help_text = help | ||||||
|         embed.add_field(name=help_title, value=help_text) |         embed.add_field(name=truncate_field_name(help_title), value=truncate_field_body(help_text)) | ||||||
|  |  | ||||||
|     return embed |     return embed | ||||||
|  |  | ||||||
|  |  | ||||||
| def status(text: str) -> discord.Embed: | def status(text: str) -> discord.Embed: | ||||||
|     embed = discord.Embed() |     embed = discord.Embed() | ||||||
|     embed.description = text |     embed.description = truncate_description(text) | ||||||
|     embed.colour = discord.Colour.blue() |     embed.colour = discord.Colour.blue() | ||||||
|     return embed |     return embed | ||||||
|  |  | ||||||
| @@ -41,7 +53,7 @@ def status(text: str) -> discord.Embed: | |||||||
| def exception_log(message_content, author_name, author_discriminator, author_id, server_id, channel_id) -> discord.Embed: | def exception_log(message_content, author_name, author_discriminator, author_id, server_id, channel_id) -> discord.Embed: | ||||||
|     embed = discord.Embed() |     embed = discord.Embed() | ||||||
|     embed.colour = discord.Colour.dark_red() |     embed.colour = discord.Colour.dark_red() | ||||||
|     embed.title = message_content |     embed.title = truncate_title(message_content) | ||||||
|  |  | ||||||
|     embed.set_footer(text="Sender: {}#{} ({}) | Server: {} | Channel: {}".format( |     embed.set_footer(text="Sender: {}#{} ({}) | Server: {} | Channel: {}".format( | ||||||
|         author_name, author_discriminator, author_id, |         author_name, author_discriminator, author_id, | ||||||
| @@ -56,30 +68,30 @@ async def system_card(conn, client: discord.Client, system: System) -> discord.E | |||||||
|     card.colour = discord.Colour.blue() |     card.colour = discord.Colour.blue() | ||||||
|  |  | ||||||
|     if system.name: |     if system.name: | ||||||
|         card.title = system.name |         card.title = truncate_title(system.name) | ||||||
|  |  | ||||||
|     if system.avatar_url: |     if system.avatar_url: | ||||||
|         card.set_thumbnail(url=system.avatar_url) |         card.set_thumbnail(url=system.avatar_url) | ||||||
|  |  | ||||||
|     if system.tag: |     if system.tag: | ||||||
|         card.add_field(name="Tag", value=system.tag) |         card.add_field(name="Tag", value=truncate_field_body(system.tag)) | ||||||
|  |  | ||||||
|     fronters, switch_time = await get_fronters(conn, system.id) |     fronters, switch_time = await get_fronters(conn, system.id) | ||||||
|     if fronters: |     if fronters: | ||||||
|         names = ", ".join([member.name for member in fronters]) |         names = ", ".join([member.name for member in fronters]) | ||||||
|         fronter_val = "{} (for {})".format(names, humanize.naturaldelta(switch_time)) |         fronter_val = "{} (for {})".format(names, humanize.naturaldelta(switch_time)) | ||||||
|         card.add_field(name="Current fronter" if len(fronters) == 1 else "Current fronters", value=fronter_val) |         card.add_field(name="Current fronter" if len(fronters) == 1 else "Current fronters", value=truncate_field_body(fronter_val)) | ||||||
|  |  | ||||||
|     account_names = [] |     account_names = [] | ||||||
|     for account_id in await system.get_linked_account_ids(conn): |     for account_id in await system.get_linked_account_ids(conn): | ||||||
|         account = await client.get_user_info(account_id) |         account = await client.get_user_info(account_id) | ||||||
|         account_names.append("{}#{}".format(account.name, account.discriminator)) |         account_names.append("{}#{}".format(account.name, account.discriminator)) | ||||||
|  |  | ||||||
|     card.add_field(name="Linked accounts", value="\n".join(account_names)) |     card.add_field(name="Linked accounts", value=truncate_field_body("\n".join(account_names))) | ||||||
|  |  | ||||||
|     if system.description: |     if system.description: | ||||||
|         card.add_field(name="Description", |         card.add_field(name="Description", | ||||||
|                        value=system.description, inline=False) |                        value=truncate_field_body(system.description), inline=False) | ||||||
|  |  | ||||||
|     # Get names of all members |     # Get names of all members | ||||||
|     all_members = await system.get_members(conn) |     all_members = await system.get_members(conn) | ||||||
| @@ -106,7 +118,7 @@ async def system_card(conn, client: discord.Client, system: System) -> discord.E | |||||||
|             field_name = "Members" |             field_name = "Members" | ||||||
|             if index >= 1: |             if index >= 1: | ||||||
|                 field_name = "Members (part {})".format(index + 1) |                 field_name = "Members (part {})".format(index + 1) | ||||||
|             card.add_field(name=field_name, value=page, inline=False) |             card.add_field(name=truncate_field_name(field_name), value=truncate_field_body(page), inline=False) | ||||||
|  |  | ||||||
|     card.set_footer(text="System ID: {}".format(system.hid)) |     card.set_footer(text="System ID: {}".format(system.hid)) | ||||||
|     return card |     return card | ||||||
| @@ -122,7 +134,7 @@ async def member_card(conn, member: Member) -> discord.Embed: | |||||||
|     if system.name: |     if system.name: | ||||||
|         name_and_system += " ({})".format(system.name) |         name_and_system += " ({})".format(system.name) | ||||||
|  |  | ||||||
|     card.set_author(name=name_and_system, icon_url=member.avatar_url or discord.Embed.Empty) |     card.set_author(name=truncate_field_name(name_and_system), icon_url=member.avatar_url or discord.Embed.Empty) | ||||||
|     if member.avatar_url: |     if member.avatar_url: | ||||||
|         card.set_thumbnail(url=member.avatar_url) |         card.set_thumbnail(url=member.avatar_url) | ||||||
|  |  | ||||||
| @@ -136,7 +148,7 @@ async def member_card(conn, member: Member) -> discord.Embed: | |||||||
|         card.add_field(name="Birthdate", value=bday_val) |         card.add_field(name="Birthdate", value=bday_val) | ||||||
|  |  | ||||||
|     if member.pronouns: |     if member.pronouns: | ||||||
|         card.add_field(name="Pronouns", value=member.pronouns) |         card.add_field(name="Pronouns", value=truncate_field_body(member.pronouns)) | ||||||
|  |  | ||||||
|     message_count = await member.message_count(conn) |     message_count = await member.message_count(conn) | ||||||
|     if message_count > 0: |     if message_count > 0: | ||||||
| @@ -146,11 +158,11 @@ async def member_card(conn, member: Member) -> discord.Embed: | |||||||
|         prefix = member.prefix or "" |         prefix = member.prefix or "" | ||||||
|         suffix = member.suffix or "" |         suffix = member.suffix or "" | ||||||
|         card.add_field(name="Proxy Tags", |         card.add_field(name="Proxy Tags", | ||||||
|                        value="{}text{}".format(prefix, suffix)) |                        value=truncate_field_body("{}text{}".format(prefix, suffix))) | ||||||
|  |  | ||||||
|     if member.description: |     if member.description: | ||||||
|         card.add_field(name="Description", |         card.add_field(name="Description", | ||||||
|                        value=member.description, inline=False) |                        value=truncate_field_body(member.description), inline=False) | ||||||
|  |  | ||||||
|     card.set_footer(text="System ID: {} | Member ID: {}".format(system.hid, member.hid)) |     card.set_footer(text="System ID: {} | Member ID: {}".format(system.hid, member.hid)) | ||||||
|     return card |     return card | ||||||
| @@ -164,9 +176,9 @@ async def front_status(switch: Switch, conn) -> discord.Embed: | |||||||
|         if len(fronter_names) == 0: |         if len(fronter_names) == 0: | ||||||
|             embed.add_field(name="Current fronter", value="(no fronter)") |             embed.add_field(name="Current fronter", value="(no fronter)") | ||||||
|         elif len(fronter_names) == 1: |         elif len(fronter_names) == 1: | ||||||
|             embed.add_field(name="Current fronter", value=fronter_names[0]) |             embed.add_field(name="Current fronter", value=truncate_field_body(fronter_names[0])) | ||||||
|         else: |         else: | ||||||
|             embed.add_field(name="Current fronters", value=", ".join(fronter_names)) |             embed.add_field(name="Current fronters", value=truncate_field_body(", ".join(fronter_names))) | ||||||
|  |  | ||||||
|         if switch.timestamp: |         if switch.timestamp: | ||||||
|             embed.add_field(name="Since", |             embed.add_field(name="Since", | ||||||
|   | |||||||
| @@ -20,13 +20,13 @@ For example: | |||||||
| `pk;system` - Shows details of your own system. | `pk;system` - Shows details of your own system. | ||||||
| `pk;system abcde` - Shows details of the system with the ID `abcde`. | `pk;system abcde` - Shows details of the system with the ID `abcde`. | ||||||
| `pk;system @JohnsAccount` - Shows details of the system linked to @JohnsAccount.""") | `pk;system @JohnsAccount` - Shows details of the system linked to @JohnsAccount.""") | ||||||
| edit_system = ("Editing system properties", """You can use the `pk;system set` command to change your system properties. The properties you can change are name, description, and tag. | edit_system = ("Editing system properties", """You can use the `pk;system` commands to change your system properties. The properties you can change are name, description, and tag. | ||||||
|          |          | ||||||
| For example: | For example: | ||||||
| `pk;system set name My System` - sets your system name to "My System". | `pk;system name My System` - sets your system name to "My System". | ||||||
| `pk;system set description A really cool system.` - sets your system description. | `pk;system description A really cool system.` - sets your system description. | ||||||
| `pk;system set tag [MS]` - Sets the tag (which will be displayed after member names in messages) to "[MS]". | `pk;system tag [MS]` - Sets the tag (which will be displayed after member names in messages) to "[MS]". | ||||||
| `pk;system set avatar https://placekitten.com/400/400` - Changes your system's avatar to a linked image. | `pk;system avatar https://placekitten.com/400/400` - Changes your system's avatar to a linked image. | ||||||
|  |  | ||||||
| If you don't specify any value, the property will be cleared.""") | If you don't specify any value, the property will be cleared.""") | ||||||
| link_account = ("Linking accounts", """If your system has multiple accounts, you can link all of them to your system, and you can use the bot from all of those accounts. | link_account = ("Linking accounts", """If your system has multiple accounts, you can link all of them to your system, and you can use the bot from all of those accounts. | ||||||
| @@ -48,17 +48,17 @@ For example: | |||||||
| `pk;member abcde` - Shows details of the member with the ID `abcde`. | `pk;member abcde` - Shows details of the member with the ID `abcde`. | ||||||
|  |  | ||||||
| You can use member IDs to look up members in other systems.""") | You can use member IDs to look up members in other systems.""") | ||||||
| edit_member = ("Editing member properties", """You can use the `pk;member set` command to change a member's properties. The properties you can change are name, description, color, pronouns, birthdate and avatar. | edit_member = ("Editing member properties", """You can use the `pk;member` commands to change a member's properties. The properties you can change are name, description, color, pronouns, birthdate and avatar. | ||||||
|  |  | ||||||
| For example: | For example: | ||||||
| `pk;member set John name Joe` - Changes John's name to Joe. | `pk;member name John Joe` - Changes John's name to Joe. | ||||||
| `pk;member set John description Pretty cool dude.` - Changes John's description. | `pk;member description John Pretty cool dude.` - Changes John's description. | ||||||
| `pk;member set John color #ff0000` - Changes John's color to red. | `pk;member color John #ff0000` - Changes John's color to red. | ||||||
| `pk;member set John pronouns he/him` - Changes John's pronouns. | `pk;member pronouns John he/him` - Changes John's pronouns. | ||||||
| `pk;member set John birthdate 1996-02-27` - Changes John's birthdate to Feb 27, 1996. (Must be YYYY-MM-DD format). | `pk;member birthdate John 1996-02-27` - Changes John's birthdate to Feb 27, 1996. (Must be YYYY-MM-DD format). | ||||||
| `pk;member set John birthdate 02-27` - Changes John's birthdate to February 27th, with no year. | `pk;member birthdate John 02-27` - Changes John's birthdate to February 27th, with no year. | ||||||
| `pk;member set John avatar https://placekitten.com/400/400` - Changes John's avatar to a linked image. | `pk;member avatar John https://placekitten.com/400/400` - Changes John's avatar to a linked image. | ||||||
| `pk;member set John avatar @JohnsAccount` - Changes John's avatar to the avatar of the mentioned account. | `pk;member avatar John @JohnsAccount` - Changes John's avatar to the avatar of the mentioned account. | ||||||
|  |  | ||||||
| If you don't specify any value, the property will be cleared.""") | If you don't specify any value, the property will be cleared.""") | ||||||
| remove_member = ("Removing a member", """If you want to delete a member, you can use the `pk;member delete` command. | remove_member = ("Removing a member", """If you want to delete a member, you can use the `pk;member delete` command. | ||||||
|   | |||||||
| @@ -68,3 +68,7 @@ class MemberNameTooLongError(PluralKitError): | |||||||
| class InvalidColorError(PluralKitError): | class InvalidColorError(PluralKitError): | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|         super().__init__("Color must be a valid hex color. (eg. #ff0000)") |         super().__init__("Color must be a valid hex color. (eg. #ff0000)") | ||||||
|  |  | ||||||
|  | class InvalidDateStringError(PluralKitError): | ||||||
|  |     def __init__(self): | ||||||
|  |         super().__init__("Invalid date string. Date must be in ISO-8601 format (YYYY-MM-DD, eg. 1999-07-25).") | ||||||
| @@ -2,7 +2,7 @@ import re | |||||||
| from datetime import date, datetime | from datetime import date, datetime | ||||||
|  |  | ||||||
| from collections.__init__ import namedtuple | from collections.__init__ import namedtuple | ||||||
| from typing import Optional | from typing import Optional, Union | ||||||
|  |  | ||||||
| from pluralkit import db, errors | from pluralkit import db, errors | ||||||
| from pluralkit.utils import validate_avatar_url_or_raise, contains_custom_emoji | from pluralkit.utils import validate_avatar_url_or_raise, contains_custom_emoji | ||||||
| @@ -59,9 +59,16 @@ class Member(namedtuple("Member", | |||||||
|         Set the name of a member. Requires the system to be passed in order to bounds check with the system tag. |         Set the name of a member. Requires the system to be passed in order to bounds check with the system tag. | ||||||
|         :raises: MemberNameTooLongError, CustomEmojiError |         :raises: MemberNameTooLongError, CustomEmojiError | ||||||
|         """ |         """ | ||||||
|  |         # Custom emojis can't go in the member name | ||||||
|  |         # Technically they *could* but they wouldn't render properly | ||||||
|  |         # so I'd rather explicitly ban them to in order to avoid confusion | ||||||
|  |  | ||||||
|  |         # The textual form is longer than the length limit in most cases | ||||||
|  |         # so we check this *before* the length check for better errors | ||||||
|         if contains_custom_emoji(new_name): |         if contains_custom_emoji(new_name): | ||||||
|             raise errors.CustomEmojiError() |             raise errors.CustomEmojiError() | ||||||
|  |  | ||||||
|  |         # Explicit name length checking | ||||||
|         if len(new_name) > system.get_member_name_limit(): |         if len(new_name) > system.get_member_name_limit(): | ||||||
|             raise errors.MemberNameTooLongError(tag_present=bool(system.tag)) |             raise errors.MemberNameTooLongError(tag_present=bool(system.tag)) | ||||||
|  |  | ||||||
| @@ -72,6 +79,7 @@ class Member(namedtuple("Member", | |||||||
|         Set or clear the description of a member. |         Set or clear the description of a member. | ||||||
|         :raises: DescriptionTooLongError |         :raises: DescriptionTooLongError | ||||||
|         """ |         """ | ||||||
|  |         # Explicit length checking | ||||||
|         if new_description and len(new_description) > 1024: |         if new_description and len(new_description) > 1024: | ||||||
|             raise errors.DescriptionTooLongError() |             raise errors.DescriptionTooLongError() | ||||||
|  |  | ||||||
| @@ -96,14 +104,31 @@ class Member(namedtuple("Member", | |||||||
|         if new_color: |         if new_color: | ||||||
|             match = re.fullmatch("#?([0-9A-Fa-f]{6})", new_color) |             match = re.fullmatch("#?([0-9A-Fa-f]{6})", new_color) | ||||||
|             if not match: |             if not match: | ||||||
|                 return errors.InvalidColorError() |                 raise errors.InvalidColorError() | ||||||
|  |  | ||||||
|             cleaned_color = match.group(1).lower() |             cleaned_color = match.group(1).lower() | ||||||
|  |  | ||||||
|         await db.update_member_field(conn, self.id, "color", cleaned_color) |         await db.update_member_field(conn, self.id, "color", cleaned_color) | ||||||
|  |  | ||||||
|     async def set_birthdate(self, conn, new_date: date): |     async def set_birthdate(self, conn, new_date: Union[date, str]): | ||||||
|         """Set or clear the birthdate of a member. To hide the birth year, pass a year of 0001.""" |         """ | ||||||
|  |         Set or clear the birthdate of a member. To hide the birth year, pass a year of 0001. | ||||||
|  |         :raises: InvalidDateStringError | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if isinstance(new_date, str): | ||||||
|  |             date_str = new_date | ||||||
|  |             try: | ||||||
|  |                 new_date = datetime.strptime(date_str, "%Y-%m-%d").date() | ||||||
|  |             except ValueError: | ||||||
|  |                 try: | ||||||
|  |                     # Try again, adding 0001 as a placeholder year | ||||||
|  |                     # This is considered a "null year" and will be omitted from the info card | ||||||
|  |                     # Useful if you want your birthday to be displayed yearless. | ||||||
|  |                     new_date = datetime.strptime("0001-" + date_str, "%Y-%m-%d").date() | ||||||
|  |                 except ValueError: | ||||||
|  |                     raise errors.InvalidDateStringError() | ||||||
|  |  | ||||||
|         await db.update_member_field(conn, self.id, "birthday", new_date) |         await db.update_member_field(conn, self.id, "birthday", new_date) | ||||||
|  |  | ||||||
|     async def set_pronouns(self, conn, new_pronouns: str): |     async def set_pronouns(self, conn, new_pronouns: str): | ||||||
|   | |||||||
| @@ -48,6 +48,7 @@ class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "a | |||||||
|         await db.update_system_field(conn, self.id, "name", new_name) |         await db.update_system_field(conn, self.id, "name", new_name) | ||||||
|  |  | ||||||
|     async def set_description(self, conn, new_description: Optional[str]): |     async def set_description(self, conn, new_description: Optional[str]): | ||||||
|  |         # Explicit length error | ||||||
|         if new_description and len(new_description) > 1024: |         if new_description and len(new_description) > 1024: | ||||||
|             raise errors.DescriptionTooLongError() |             raise errors.DescriptionTooLongError() | ||||||
|  |  | ||||||
| @@ -55,12 +56,14 @@ class System(namedtuple("System", ["id", "hid", "name", "description", "tag", "a | |||||||
|  |  | ||||||
|     async def set_tag(self, conn, new_tag: Optional[str]): |     async def set_tag(self, conn, new_tag: Optional[str]): | ||||||
|         if new_tag: |         if new_tag: | ||||||
|  |             # Explicit length error | ||||||
|             if len(new_tag) > 32: |             if len(new_tag) > 32: | ||||||
|                 raise errors.TagTooLongError() |                 raise errors.TagTooLongError() | ||||||
|  |  | ||||||
|             if contains_custom_emoji(new_tag): |             if contains_custom_emoji(new_tag): | ||||||
|                 raise errors.CustomEmojiError() |                 raise errors.CustomEmojiError() | ||||||
|  |  | ||||||
|  |             # Check name+tag length for all members | ||||||
|             members_exceeding = await db.get_members_exceeding(conn, system_id=self.id, length=32 - len(new_tag) - 1) |             members_exceeding = await db.get_members_exceeding(conn, system_id=self.id, length=32 - len(new_tag) - 1) | ||||||
|             if len(members_exceeding) > 0: |             if len(members_exceeding) > 0: | ||||||
|                 raise errors.TagTooLongWithMembersError([member.name for member in members_exceeding]) |                 raise errors.TagTooLongWithMembersError([member.name for member in members_exceeding]) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user