From 46bf03ae25b54f9acea10dbb2892f2864dd39154 Mon Sep 17 00:00:00 2001 From: Conatum Date: Wed, 29 Dec 2021 20:20:17 +0200 Subject: [PATCH] feature (setprofile): Profile tag editor. --- bot/modules/stats/setprofile.py | 216 +++++++++++++++++++++++++++++ data/migration/v6-v7/migration.sql | 18 ++- data/schema.sql | 17 ++- 3 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 bot/modules/stats/setprofile.py diff --git a/bot/modules/stats/setprofile.py b/bot/modules/stats/setprofile.py new file mode 100644 index 00000000..766eb5ea --- /dev/null +++ b/bot/modules/stats/setprofile.py @@ -0,0 +1,216 @@ +""" +Provides a command to update a member's profile badges. +""" +import string +import discord + +from cmdClient.lib import SafeCancellation +from cmdClient.checks import in_guild +from wards import guild_moderator + +from .data import profile_tags +from .module import module + + +MAX_TAGS = 5 +MAX_LENGTH = 30 + + +@module.cmd( + "setprofile", + group="Personal Settings", + desc="Set or update your study profile tags.", + aliases=('editprofile', 'mytags'), + flags=('clear', 'for') +) +@in_guild() +async def cmd_setprofile(ctx, flags): + """ + Usage``: + {prefix}setprofile , , , ... + {prefix}setprofile + {prefix}setprofile --clear [--for @user] + Description: + Set or update the tags appearing in your study server profile. + + You can have at most `5` tags at once. + + Moderators can clear a user's tags with `--clear --for @user`. + Examples``: + {prefix}setprofile Mathematics, Bioloyg, Medicine, Undergraduate, Europe + {prefix}setprofile 2 Biology + {prefix}setprofile --clear + """ + if flags['clear']: + if flags['for']: + # Moderator-clearing a user's tags + # First check moderator permissions + if not await guild_moderator.run(ctx): + return await ctx.error_reply( + "You need to be a server moderator to use this!" + ) + + # Check input and extract users to clear for + if not (users := ctx.msg.mentions): + # Show moderator usage + return await ctx.error_reply( + f"**Usage:** `{ctx.best_prefix}setprofile --clear --for @user`\n" + f"**Example:** {ctx.best_prefix}setprofile --clear --for {ctx.author.mention}" + ) + + # Clear the tags + profile_tags.delete_where( + guildid=ctx.guild.id, + userid=[user.id for user in users] + ) + + # Ack the moderator + await ctx.embed_reply( + "Profile tags cleared!" + ) + else: + # The author wants to clear their own tags + + # First delete the tags, save the rows for reporting + rows = profile_tags.delete_where( + guildid=ctx.guild.id, + userid=ctx.author.id + ) + + # Ack the user + if not rows: + await ctx.embed_reply( + "You don't have any profile tags to clear!" + ) + else: + embed = discord.Embed( + colour=discord.Colour.green(), + description="Successfully cleared your profile!" + ) + embed.add_field( + name="Removed tags", + value='\n'.join(row['tag'].upper() for row in rows) + ) + await ctx.reply(embed=embed) + elif ctx.args: + if len(splits := ctx.args.split(maxsplit=1)) > 1 and splits[0].isdigit(): + # Assume we are editing the provided id + tagid = int(splits[0]) + if tagid > MAX_TAGS: + return await ctx.error_reply( + f"Sorry, you can have a maximum of `{MAX_TAGS}` tags!" + ) + + # Retrieve the user's current taglist + rows = profile_tags.select_where( + guildid=ctx.guild.id, + userid=ctx.author.id, + _extra="ORDER BY tagid ASC" + ) + + # Parse and validate provided new content + content = splits[1].strip().upper() + validate_tag(content) + + if tagid > len(rows): + # Trying to edit a tag that doesn't exist yet + # Just create it instead + profile_tags.insert( + guildid=ctx.guild.id, + userid=ctx.author.id, + tag=content + ) + + # Ack user + await ctx.reply( + embed=discord.Embed(title="Tag created!", colour=discord.Colour.green()) + ) + else: + # Get the row id to update + to_edit = rows[tagid - 1]['tagid'] + + # Update the tag + profile_tags.update_where( + {'tag': content}, + tagid=to_edit + ) + + # Ack user + embed = discord.Embed( + colour=discord.Colour.green(), + title="Tag updated!" + ) + await ctx.reply(embed=embed) + else: + # Assume the arguments are a comma separated list of badges + # Parse and validate + to_add = [split.strip().upper() for split in ctx.args.split(',')] + validate_tag(*to_add) + + # Remove the existing badges + deleted_rows = profile_tags.delete_where( + guildid=ctx.guild.id, + userid=ctx.author.id + ) + + # Insert the new tags + profile_tags.insert_many( + *((ctx.guild.id, ctx.author.id, tag) for tag in to_add), + insert_keys=('guildid', 'userid', 'tag') + ) + + # Ack with user + embed = discord.Embed( + colour=discord.Colour.green(), + description="Profile tags updated!" + ) + embed.add_field( + name="New tags", + value='\n'.join(to_add) + ) + if deleted_rows: + embed.add_field( + name="Previous tags", + value='\n'.join(row['tag'].upper() for row in deleted_rows), + inline=False + ) + await ctx.reply(embed=embed) + else: + # No input was provided + # Show usage and exit + embed = discord.Embed( + colour=discord.Colour.red(), + description=( + "Use this command to edit your study profile " + "tags so other people can see what you do!" + ) + ) + embed.add_field( + name="Usage", + value=( + f"`{ctx.best_prefix}setprofile , , , ...`\n" + f"`{ctx.best_prefix}setprofile `" + ) + ) + embed.add_field( + name="Examples", + value=( + f"`{ctx.best_prefix}setprofile Mathematics, Bioloyg, Medicine, Undergraduate, Europe`\n" + f"`{ctx.best_prefix}setprofile 2 Biology`" + ), + inline=False + ) + await ctx.reply(embed=embed) + + +def validate_tag(*content): + for content in content: + if not set(content).issubset(string.printable): + raise SafeCancellation( + f"Invalid tag `{content}`!\n" + "Tags may only contain alphanumeric and punctuation characters." + ) + if len(content) > MAX_LENGTH: + raise SafeCancellation( + f"Provided tag is too long! Please keep your tags shorter than {MAX_LENGTH} characters." + ) diff --git a/data/migration/v6-v7/migration.sql b/data/migration/v6-v7/migration.sql index 04cc39da..3d4db031 100644 --- a/data/migration/v6-v7/migration.sql +++ b/data/migration/v6-v7/migration.sql @@ -1,7 +1,4 @@ --- Add deletion column to tasklist entries --- Add completed_at column to the tasklist entries, replacing complete - - +-- Improved tasklist statistics ALTER TABLE tasklist ADD COLUMN completed_at TIMESTAMPTZ, ADD COLUMN deleted_at TIMESTAMPTZ, @@ -15,4 +12,15 @@ ALTER TABLE tasklist DROP COLUMN complete; --- Mark all tasklist entries older than a day as deleted +-- New member profile tags +CREATE TABLE member_profile_tags( + tagid SERIAL PRIMARY KEY, + guildid BIGINT NOT NULL, + userid BIGINT NOT NULL, + tag TEXT NOT NULL, + _timestamp TIMESTAMPTZ DEFAULT now(), + FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) +); + + +INSERT INTO VersionHistory (version, author) VALUES (7, 'v6-v7 migration'); diff --git a/data/schema.sql b/data/schema.sql index cf960112..5edb3add 100644 --- a/data/schema.sql +++ b/data/schema.sql @@ -4,7 +4,7 @@ CREATE TABLE VersionHistory( time TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, author TEXT ); -INSERT INTO VersionHistory (version, author) VALUES (6, 'Initial Creation'); +INSERT INTO VersionHistory (version, author) VALUES (7, 'Initial Creation'); CREATE OR REPLACE FUNCTION update_timestamp_column() @@ -683,4 +683,19 @@ CREATE TABLE past_member_roles( CREATE INDEX member_role_persistence_members ON past_member_roles (guildid, userid); -- }}} +-- Member profile tags {{{ +CREATE TABLE member_profile_tags( + tagid SERIAL PRIMARY KEY, + guildid BIGINT NOT NULL, + userid BIGINT NOT NULL, + tag TEXT NOT NULL, + _timestamp TIMESTAMPTZ DEFAULT now(), + FOREIGN KEY (guildid, userid) REFERENCES members (guildid, userid) +); +-- }}} + +-- Member goals {{{ + +-- }}} + -- vim: set fdm=marker: