System/member set command rework, should be more user friendly now

This commit is contained in:
Ske 2018-11-23 21:55:47 +01:00
parent 8e504fa879
commit 10746ae807
8 changed files with 183 additions and 119 deletions

View File

@ -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),

View File

@ -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.")

View File

@ -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:

View File

@ -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",

View File

@ -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.

View File

@ -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).")

View File

@ -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):

View File

@ -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])