From c70dd62306b891879b9d12c0c92529338cb6c7eb Mon Sep 17 00:00:00 2001 From: Luna Mendes Date: Sat, 17 Nov 2018 17:52:34 -0300 Subject: [PATCH] guild: add implementations for emoji add/update/remove All icons will be invalidated. - images: change icon path model - images: handle hashes being NULL, for emojis only needing the key - schemas: add NEW_EMOJI, PATCH_EMOJI - migration.scripts: add 3_drop_constraints_icons_hash - schema.sql: drop unique and not null from hash, change primary key in icons --- litecord/blueprints/guild/emoji.py | 86 ++++++++++++++++++- litecord/images.py | 34 +++++--- litecord/schemas.py | 13 +++ .../scripts/3_drop_constraints_icons_hash.sql | 9 ++ schema.sql | 4 +- 5 files changed, 128 insertions(+), 18 deletions(-) create mode 100644 manage/cmd/migration/scripts/3_drop_constraints_icons_hash.sql diff --git a/litecord/blueprints/guild/emoji.py b/litecord/blueprints/guild/emoji.py index 561378e..0720662 100644 --- a/litecord/blueprints/guild/emoji.py +++ b/litecord/blueprints/guild/emoji.py @@ -1,11 +1,21 @@ -from quart import Blueprint, jsonify, current_app as app +from quart import Blueprint, jsonify, current_app as app, request from litecord.auth import token_check -from litecord.blueprints.checks import guild_check +from litecord.blueprints.checks import guild_check, guild_perm_check +from litecord.schemas import validate, NEW_EMOJI, PATCH_EMOJI +from litecord.snowflake import get_snowflake bp = Blueprint('guild.emoji', __name__) +async def _dispatch_emojis(guild_id): + """Dispatch a Guild Emojis Update payload to a guild.""" + await app.dispatcher.dispatch('guild', guild_id, 'GUILD_EMOJIS_UPDATE', { + 'guild_id': str(guild_id), + 'emojis': await app.storage.get_guild_emojis(guild_id) + }) + + @bp.route('//emojis', methods=['GET']) async def _get_guild_emoji(guild_id): user_id = await token_check() @@ -22,3 +32,75 @@ async def _get_guild_emoji_one(guild_id, emoji_id): return jsonify( await app.storage.get_emoji(emoji_id) ) + + +@bp.route('//emojis', methods=['POST']) +async def _put_emoji(guild_id): + user_id = await token_check() + + await guild_check(user_id, guild_id) + await guild_perm_check(user_id, guild_id, 'manage_emojis') + + j = validate(await request.get_json(), NEW_EMOJI) + + emoji_id = get_snowflake() + + icon = await app.icons.put('emoji', emoji_id, None, j['image']) + + await app.db.execute( + """ + INSERT INTO guild_emoji + (id, guild_id, uploader_id, name, image, animated) + VALUES + ($1, $2, $3, $4, $5, $6) + """, + emoji_id, guild_id, user_id, + j['name'], + icon.icon_hash, + icon.mime == 'image/gif') + + await _dispatch_emojis(guild_id) + + return jsonify( + await app.storage.get_emoji(emoji_id) + ) + + +@bp.route('//emojis/', methods=['PATCH']) +async def _patch_emoji(guild_id, emoji_id): + user_id = await token_check() + + await guild_check(user_id, guild_id) + await guild_perm_check(user_id, guild_id, 'manage_emojis') + + j = validate(await request.get_json(), PATCH_EMOJI) + + # TODO: check if it actually updated anything + await app.db.execute(""" + UPDATE guild_emoji + SET name = $1 + WHERE id = $2 + """, j['name'], emoji_id) + + await _dispatch_emojis(guild_id) + + return jsonify( + await app.storage.get_emoji(emoji_id) + ) + + +@bp.route('//emojis/', methods=['DELETE']) +async def _del_emoji(guild_id, emoji_id): + user_id = await token_check() + + await guild_check(user_id, guild_id) + await guild_perm_check(user_id, guild_id, 'manage_emojis') + + # TODO: check if actually deleted + await app.db.execute(""" + DELETE FROM guild_emoji + WHERE id = $2 + """, emoji_id) + + await _dispatch_emojis(guild_id) + return '', 204 diff --git a/litecord/images.py b/litecord/images.py index 6d8235a..1fa2bbd 100644 --- a/litecord/images.py +++ b/litecord/images.py @@ -26,6 +26,7 @@ def _get_mime(ext: str): @dataclass class Icon: """Main icon class""" + key: str icon_hash: str mime: str @@ -33,7 +34,7 @@ class Icon: def as_path(self) -> str: """Return a filesystem path for the given icon.""" ext = _get_ext(self.mime) - return str(IMAGE_FOLDER / f'{self.icon_hash}.{ext}') + return str(IMAGE_FOLDER / f'{self.key}_{self.icon_hash}.{ext}') @property def as_pathlib(self) -> str: @@ -147,40 +148,42 @@ class IconManager: target_mime = _get_mime(target) log.info('converting from {} to {}', icon.mime, target_mime) - target_path = IMAGE_FOLDER / f'{icon.icon_hash}.{target}' + target_path = IMAGE_FOLDER / f'{icon.key}_{icon.icon_hash}.{target}' if target_path.exists(): - return Icon(icon.icon_hash, target_mime) + return Icon(icon.key, icon.icon_hash, target_mime) image = Image.open(icon.as_path) target_fd = target_path.open('wb') image.save(target_fd, format=target) target_fd.close() - return Icon(icon.icon_hash, target_mime) + return Icon(icon.key, icon.icon_hash, target_mime) async def generic_get(self, scope, key, icon_hash, **kwargs) -> Icon: """Get any icon.""" log.debug('GET {} {} {}', scope, key, icon_hash) key = str(key) - icon_row = await self.storage.db.fetchrow(""" - SELECT hash, mime + hash_query = 'AND hash = $3' if key else '' + + icon_row = await self.storage.db.fetchrow(f""" + SELECT key, hash, mime FROM icons WHERE scope = $1 AND key = $2 - AND hash = $3 + {hash_query} """, scope, key, icon_hash) if not icon_row: return None - icon = Icon(icon_row['hash'], icon_row['mime']) + icon = Icon(icon_row['key'], icon_row['hash'], icon_row['mime']) if not icon.as_pathlib.exists(): await self.delete(icon) return None - + if 'ext' in kwargs and kwargs['ext'] != icon.extension: return await self._convert_ext(icon, kwargs['ext']) @@ -195,7 +198,7 @@ class IconManager: b64_data: str, **kwargs) -> Icon: """Insert an icon.""" if b64_data is None: - return Icon(None, '') + return Icon(None, None, '') mime, raw_data = parse_data_uri(b64_data) data_fd = BytesIO(raw_data) @@ -223,7 +226,10 @@ class IconManager: data_fd.seek(0) # calculate sha256 - icon_hash = await calculate_hash(data_fd) + # ignore icon hashes if we're talking about emoji + icon_hash = (await calculate_hash(data_fd) + if scope != 'emoji' + else None) await self.storage.db.execute(""" INSERT INTO icons (scope, key, hash, mime) @@ -231,14 +237,14 @@ class IconManager: """, scope, str(key), icon_hash, mime) # write it off to fs - icon_path = IMAGE_FOLDER / f'{icon_hash}.{extension}' + icon_path = IMAGE_FOLDER / f'{key}_{icon_hash}.{extension}' icon_path.write_bytes(raw_data) # copy from data_fd to icon_fd # with icon_path.open(mode='wb') as icon_fd: # icon_fd.write(data_fd.read()) - return Icon(icon_hash, mime) + return Icon(str(key), icon_hash, mime) async def delete(self, icon: Icon): """Delete an icon from the database and filesystem.""" @@ -274,7 +280,7 @@ class IconManager: WHERE hash = $1 """, icon.icon_hash) - paths = IMAGE_FOLDER.glob(f'{icon.icon_hash}.*') + paths = IMAGE_FOLDER.glob(f'{icon.key}_{icon.icon_hash}.*') for path in paths: try: diff --git a/litecord/schemas.py b/litecord/schemas.py index a5c4460..4a392d0 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -566,3 +566,16 @@ GUILD_SETTINGS = { GUILD_PRUNE = { 'days': {'type': 'number', 'coerce': int, 'min': 1} } + +NEW_EMOJI = { + 'name': { + 'type': 'string', 'minlength': 1, 'maxlength': 256, 'required': True}, + 'image': {'type': 'b64_icon', 'required': True}, + 'roles': {'type': 'list', 'schema': {'coerce': int}} +} + +PATCH_EMOJI = { + 'name': { + 'type': 'string', 'minlength': 1, 'maxlength': 256, 'required': True}, + 'roles': {'type': 'list', 'schema': {'coerce': int}} +} diff --git a/manage/cmd/migration/scripts/3_drop_constraints_icons_hash.sql b/manage/cmd/migration/scripts/3_drop_constraints_icons_hash.sql new file mode 100644 index 0000000..59ef69e --- /dev/null +++ b/manage/cmd/migration/scripts/3_drop_constraints_icons_hash.sql @@ -0,0 +1,9 @@ +-- drop main primary key +-- since hash can now be nullable +ALTER TABLE icons DROP CONSTRAINT "icons_pkey"; + +-- remove not null from hash column +ALTER TABLE icons ALTER COLUMN hash DROP NOT NULL; + +-- add new primary key, without hash +ALTER TABLE icons ADD CONSTRAINT icons_pkey PRIMARY KEY (scope, key); diff --git a/schema.sql b/schema.sql index 4f25172..f4a05d1 100644 --- a/schema.sql +++ b/schema.sql @@ -49,11 +49,11 @@ CREATE TABLE IF NOT EXISTS icons ( key text, -- sha256 of the icon - hash text UNIQUE NOT NULL, + hash text, -- icon mime mime text NOT NULL, - PRIMARY KEY (scope, hash, mime) + PRIMARY KEY (scope, key) );