From a04b3acfaae051b94f2e0b38443ab7a6173cbfff Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 9 Mar 2019 03:27:12 -0300 Subject: [PATCH 01/35] storage: add guild.features in get_guild - remove features and guild_features tables --- litecord/storage.py | 5 +--- .../scripts/12_remove_features_table.sql | 4 ++++ schema.sql | 23 ++++--------------- 3 files changed, 10 insertions(+), 22 deletions(-) create mode 100644 manage/cmd/migration/scripts/12_remove_features_table.sql diff --git a/litecord/storage.py b/litecord/storage.py index 1530a65..0c8f6d4 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -155,7 +155,7 @@ class Storage: explicit_content_filter, mfa_level, embed_enabled, embed_channel_id::text, widget_enabled, widget_channel_id::text, - system_channel_id::text + system_channel_id::text, features FROM guilds WHERE guilds.id = $1 """, guild_id) @@ -918,9 +918,6 @@ class Storage: if guild: dinv['guild'] = dict(guild) - - # TODO: query actual guild features - dinv['guild']['features'] = [] else: dinv['guild'] = {} diff --git a/manage/cmd/migration/scripts/12_remove_features_table.sql b/manage/cmd/migration/scripts/12_remove_features_table.sql new file mode 100644 index 0000000..5f37056 --- /dev/null +++ b/manage/cmd/migration/scripts/12_remove_features_table.sql @@ -0,0 +1,4 @@ +DROP TABLE guild_features; +DROP TABLE features; + +ALTER TABLE guilds ADD COLUMN features text[]; diff --git a/schema.sql b/schema.sql index 269ff91..0f5742d 100644 --- a/schema.sql +++ b/schema.sql @@ -362,12 +362,13 @@ CREATE TABLE IF NOT EXISTS guilds ( region text NOT NULL REFERENCES voice_regions (id), - /* default no afk channel - afk channel is voice-only. - */ + features text[], + + -- default no afk channel + -- afk channel is voice-only. afk_channel_id bigint REFERENCES channels (id) DEFAULT NULL, - /* default 5 minutes */ + -- default 5 minutes afk_timeout int DEFAULT 300, -- from 0 to 4 @@ -482,20 +483,6 @@ CREATE TABLE IF NOT EXISTS group_dm_members ( ); - - -CREATE TABLE IF NOT EXISTS features ( - id serial PRIMARY KEY, - feature text NOT NULL -); - -CREATE TABLE IF NOT EXISTS guild_features ( - guild_id bigint REFERENCES guilds (id) ON DELETE CASCADE, - feature integer REFERENCES features (id), - PRIMARY KEY (guild_id, feature) -); - - CREATE TABLE IF NOT EXISTS guild_integrations ( guild_id bigint REFERENCES guilds (id) ON DELETE CASCADE, user_id bigint REFERENCES users (id) ON DELETE CASCADE, From 073f47e0f5620f5fc5382748e75d05fbeb946c81 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 9 Mar 2019 04:09:55 -0300 Subject: [PATCH 02/35] add draft for features_admin and add guilds_admin --- litecord/blueprints/admin_api/__init__.py | 4 +- litecord/blueprints/admin_api/features.py | 49 +++++++++++++++++++++++ litecord/blueprints/admin_api/guilds.py | 33 +++++++++++++++ run.py | 7 +++- 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 litecord/blueprints/admin_api/features.py create mode 100644 litecord/blueprints/admin_api/guilds.py diff --git a/litecord/blueprints/admin_api/__init__.py b/litecord/blueprints/admin_api/__init__.py index d209cf9..32710ce 100644 --- a/litecord/blueprints/admin_api/__init__.py +++ b/litecord/blueprints/admin_api/__init__.py @@ -18,5 +18,7 @@ along with this program. If not, see . """ from .voice import bp as voice +from .features import bp as features +from .guilds import bp as guilds -__all__ = ['voice'] +__all__ = ['voice', 'features', 'guilds'] diff --git a/litecord/blueprints/admin_api/features.py b/litecord/blueprints/admin_api/features.py new file mode 100644 index 0000000..ca14309 --- /dev/null +++ b/litecord/blueprints/admin_api/features.py @@ -0,0 +1,49 @@ +""" + +Litecord +Copyright (C) 2018-2019 Luna Mendes + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +""" + +from quart import Blueprint, current_app as app + +from litecord.auth import admin_check +from litecord.errors import BadRequest + +bp = Blueprint('features_admin', __name__) + +FEATURES = [ + '' +] + +@bp.route('//', methods=['PUT']) +async def insert_feature(guild_id: int, feature: str): + """Insert a feature on a guild.""" + await admin_check() + + # TODO + if feature not in FEATURES: + raise BadRequest('invalid feature') + + return '', 204 + + +@bp.route('//', methods=['DELETE']) +async def remove_feature(guild_id: int, feature: str): + """Remove a feature from a guild""" + await admin_check() + # TODO + await app.db + return '', 204 diff --git a/litecord/blueprints/admin_api/guilds.py b/litecord/blueprints/admin_api/guilds.py new file mode 100644 index 0000000..d779c02 --- /dev/null +++ b/litecord/blueprints/admin_api/guilds.py @@ -0,0 +1,33 @@ +""" + +Litecord +Copyright (C) 2018-2019 Luna Mendes + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, version 3 of the License. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +""" + +from quart import Blueprint, jsonify, current_app as app + +from litecord.auth import admin_check + +bp = Blueprint('guilds_admin', __name__) + +@bp.route('/', methods=['GET']) +async def get_guild(guild_id: int): + """Get a basic guild payload.""" + await admin_check() + + return jsonify( + await app.storage.get_guild(guild_id) + ) diff --git a/run.py b/run.py index 398896d..8441f28 100644 --- a/run.py +++ b/run.py @@ -57,7 +57,8 @@ from litecord.blueprints.user import ( from litecord.blueprints.user.billing_job import payment_job from litecord.blueprints.admin_api import ( - voice as voice_admin + voice as voice_admin, features as features_admin, + guilds as guilds_admin ) from litecord.blueprints.admin_api.voice import guild_region_check @@ -143,7 +144,9 @@ def set_blueprints(app_): nodeinfo: -1, static: -1, - voice_admin: '/admin/voice' + voice_admin: '/admin/voice', + features_admin: '/admin/features', + guilds_admin: '/admin/guilds' } for bp, suffix in bps.items(): From 2c1c384409009134e757110899b6492f4abcfd13 Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 11 Mar 2019 01:36:52 -0300 Subject: [PATCH 03/35] admin_api.features: make features input as a list - add PATCH /:id/features - enums: add Feature enum - schemas: add FEATURES - storage: add Storage.guild_features - types: add return type to timestamp_ --- litecord/blueprints/admin_api/features.py | 70 ++++++++++++++++++----- litecord/enums.py | 15 +++++ litecord/schemas.py | 7 +++ litecord/storage.py | 7 +++ litecord/types.py | 5 +- 5 files changed, 89 insertions(+), 15 deletions(-) diff --git a/litecord/blueprints/admin_api/features.py b/litecord/blueprints/admin_api/features.py index ca14309..b786692 100644 --- a/litecord/blueprints/admin_api/features.py +++ b/litecord/blueprints/admin_api/features.py @@ -17,33 +17,75 @@ along with this program. If not, see . """ -from quart import Blueprint, current_app as app +from quart import Blueprint, current_app as app, jsonify, request from litecord.auth import admin_check from litecord.errors import BadRequest +from litecord.schemas import validate, FEATURES +from litecord.enums import Feature bp = Blueprint('features_admin', __name__) -FEATURES = [ - '' -] -@bp.route('//', methods=['PUT']) -async def insert_feature(guild_id: int, feature: str): +async def _features(guild_id: int): + return jsonify({ + 'features': await app.storage.guild_features(guild_id) + }) + + +async def _update_features(guild_id: int, features: list): + await app.db.execute(""" + UPDATE guilds + SET features = $1 + WHERE id = $2 + """, features, guild_id) + + +@bp.route('//features', methods=['PATCH']) +async def replace_features(guild_id: int): + """Replace the feature list in a guild""" + await admin_check() + j = validate(await request.get_json(), FEATURES) + features = j['features'] + + # yes, we need to pass it to a set and then to a list before + # doing anything, since the api client might just + # shove 200 repeated features to us. + await _update_features(guild_id, list(set(features))) + return await _features(guild_id) + + +@bp.route('//features', methods=['PUT']) +async def insert_features(guild_id: int): """Insert a feature on a guild.""" await admin_check() + j = validate(await request.get_json(), FEATURES) - # TODO - if feature not in FEATURES: - raise BadRequest('invalid feature') + to_add = j['features'] + features = await app.storage.guild_features(guild_id) + features = set(features) - return '', 204 + # i'm assuming set.add is mostly safe + for feature in to_add: + features.add(feature) + + await _update_features(guild_id, list(features)) + return await _features(guild_id) @bp.route('//', methods=['DELETE']) -async def remove_feature(guild_id: int, feature: str): +async def remove_feature(guild_id: int): """Remove a feature from a guild""" await admin_check() - # TODO - await app.db - return '', 204 + j = validate(await request.get_json(), FEATURES) + to_remove = j['features'] + features = await app.storage.guild_features(guild_id) + + for feature in to_remove: + try: + features.remove(feature) + except ValueError: + raise BadRequest('Trying to remove already removed feature.') + + await _update_features(guild_id, features) + return await _features(guild_id) diff --git a/litecord/enums.py b/litecord/enums.py index 4107fd5..0a7c075 100644 --- a/litecord/enums.py +++ b/litecord/enums.py @@ -197,12 +197,27 @@ class RelationshipType(EasyEnum): class MessageNotifications(EasyEnum): + """Message notifications""" ALL = 0 MENTIONS = 1 NOTHING = 2 class PremiumType: + """Premium (Nitro) type.""" TIER_1 = 1 TIER_2 = 2 NONE = None + + +class Feature(Enum): + """Guild features.""" + invite_splash = 'INVITE_SPLASH' + vip = 'VIP_REGIONS' + vanity = 'VANITY_URL' + emoji = 'MORE_EMOJI' + verified = 'VERIFIED' + + # unknown + commerce = 'COMMERCE' + news = 'NEWS' diff --git a/litecord/schemas.py b/litecord/schemas.py index 3683334..40499da 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -655,3 +655,10 @@ GET_MENTIONS = { 'everyone': {'coerce': bool, 'default': True}, 'guild_id': {'coerce': int, 'required': False} } + +FEATURES = { + 'features': { + 'type': 'list', 'required': True, + 'schema': {'type': 'guild_feature'} + } +} diff --git a/litecord/storage.py b/litecord/storage.py index 0c8f6d4..d3b2257 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -146,6 +146,13 @@ class Storage: WHERE username = $1 AND discriminator = $2 """, username, discriminator) + async def guild_features(self, guild_id: int) -> Optional[List[str]]: + """Get a list of guild features for the given guild.""" + return await self.db.fetchval(""" + SELECT features FROM guilds + WHERE id = $1 + """, guild_id) + async def get_guild(self, guild_id: int, user_id=None) -> Optional[Dict]: """Get gulid payload.""" row = await self.db.fetchrow(""" diff --git a/litecord/types.py b/litecord/types.py index 1a845f8..2c9c122 100644 --- a/litecord/types.py +++ b/litecord/types.py @@ -17,6 +17,8 @@ along with this program. If not, see . """ +from typing import Optional + # size units KILOBYTES = 1024 @@ -45,5 +47,6 @@ class Color: return self.value -def timestamp_(dt): +def timestamp_(dt) -> Optional[str]: + """safer version for dt.isoformat()""" return f'{dt.isoformat()}+00:00' if dt else None From c325543242c932943d4b555c4737bbea1a49ab41 Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 11 Mar 2019 01:40:18 -0300 Subject: [PATCH 04/35] enums: move Feature to EasyEnum - schemas: add validator for guild_features type --- litecord/enums.py | 8 ++++++-- litecord/schemas.py | 5 ++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/litecord/enums.py b/litecord/enums.py index 0a7c075..52cd742 100644 --- a/litecord/enums.py +++ b/litecord/enums.py @@ -18,12 +18,16 @@ along with this program. If not, see . """ import inspect +from typing import List, Any from enum import Enum, IntEnum class EasyEnum(Enum): + """Wrapper around the enum class for convenience.""" + @classmethod - def values(cls): + def values(cls) -> List[Any]: + """Return list of values for the given enum.""" return [v.value for v in cls.__members__.values()] @@ -210,7 +214,7 @@ class PremiumType: NONE = None -class Feature(Enum): +class Feature(EasyEnum): """Guild features.""" invite_splash = 'INVITE_SPLASH' vip = 'VIP_REGIONS' diff --git a/litecord/schemas.py b/litecord/schemas.py index 40499da..54bed7c 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -28,7 +28,7 @@ from .permissions import Permissions from .types import Color from .enums import ( ActivityType, StatusType, ExplicitFilter, RelationshipType, - MessageNotifications, ChannelType, VerificationLevel + MessageNotifications, ChannelType, VerificationLevel, Features ) from litecord.embed.schemas import EMBED_OBJECT @@ -145,6 +145,9 @@ class LitecordValidator(Validator): def _validate_type_nickname(self, value: str) -> bool: return isinstance(value, str) and (len(value) < 32) + def _validate_type_guild_feature(self, value: str) -> bool: + return value in Features.values() + def validate(reqjson: Union[Dict, List], schema: Dict, raise_err: bool = True) -> Dict: From 65b4b28d89ceabb61288a4420f4f18c39cce2906 Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 11 Mar 2019 01:43:11 -0300 Subject: [PATCH 05/35] schemas: use coerce to Feature instead of guild_features type cleaner to write code for. - admin_api.features: support that --- litecord/blueprints/admin_api/features.py | 15 +++++++++------ litecord/schemas.py | 7 ++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/litecord/blueprints/admin_api/features.py b/litecord/blueprints/admin_api/features.py index b786692..40a4bf1 100644 --- a/litecord/blueprints/admin_api/features.py +++ b/litecord/blueprints/admin_api/features.py @@ -16,6 +16,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from typing import List from quart import Blueprint, current_app as app, jsonify, request @@ -27,6 +28,11 @@ from litecord.enums import Feature bp = Blueprint('features_admin', __name__) +async def _features_from_req() -> List[str]: + j = validate(await request.get_json(), FEATURES) + return [feature.value for feature in j['features']] + + async def _features(guild_id: int): return jsonify({ 'features': await app.storage.guild_features(guild_id) @@ -45,8 +51,7 @@ async def _update_features(guild_id: int, features: list): async def replace_features(guild_id: int): """Replace the feature list in a guild""" await admin_check() - j = validate(await request.get_json(), FEATURES) - features = j['features'] + features = await _features_from_req() # yes, we need to pass it to a set and then to a list before # doing anything, since the api client might just @@ -59,9 +64,8 @@ async def replace_features(guild_id: int): async def insert_features(guild_id: int): """Insert a feature on a guild.""" await admin_check() - j = validate(await request.get_json(), FEATURES) + to_add = await _features_from_req() - to_add = j['features'] features = await app.storage.guild_features(guild_id) features = set(features) @@ -77,8 +81,7 @@ async def insert_features(guild_id: int): async def remove_feature(guild_id: int): """Remove a feature from a guild""" await admin_check() - j = validate(await request.get_json(), FEATURES) - to_remove = j['features'] + to_remove = await _features_from_req() features = await app.storage.guild_features(guild_id) for feature in to_remove: diff --git a/litecord/schemas.py b/litecord/schemas.py index 54bed7c..a746bfe 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -28,7 +28,7 @@ from .permissions import Permissions from .types import Color from .enums import ( ActivityType, StatusType, ExplicitFilter, RelationshipType, - MessageNotifications, ChannelType, VerificationLevel, Features + MessageNotifications, ChannelType, VerificationLevel, Feature ) from litecord.embed.schemas import EMBED_OBJECT @@ -145,9 +145,6 @@ class LitecordValidator(Validator): def _validate_type_nickname(self, value: str) -> bool: return isinstance(value, str) and (len(value) < 32) - def _validate_type_guild_feature(self, value: str) -> bool: - return value in Features.values() - def validate(reqjson: Union[Dict, List], schema: Dict, raise_err: bool = True) -> Dict: @@ -662,6 +659,6 @@ GET_MENTIONS = { FEATURES = { 'features': { 'type': 'list', 'required': True, - 'schema': {'type': 'guild_feature'} + 'schema': {'coerce': Feature} } } From 87781c4606d8e801a42c4540dd274924c878a9b7 Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 11 Mar 2019 01:45:49 -0300 Subject: [PATCH 06/35] admin_api.features: remove unused import --- litecord/blueprints/admin_api/features.py | 1 - 1 file changed, 1 deletion(-) diff --git a/litecord/blueprints/admin_api/features.py b/litecord/blueprints/admin_api/features.py index 40a4bf1..7ee7a0c 100644 --- a/litecord/blueprints/admin_api/features.py +++ b/litecord/blueprints/admin_api/features.py @@ -23,7 +23,6 @@ from quart import Blueprint, current_app as app, jsonify, request from litecord.auth import admin_check from litecord.errors import BadRequest from litecord.schemas import validate, FEATURES -from litecord.enums import Feature bp = Blueprint('features_admin', __name__) From 4bb3cad43e1f9e126ab93afdcd203041c146469b Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 11 Mar 2019 02:04:07 -0300 Subject: [PATCH 07/35] migration.scripts: add NOT NULL and DEFAULT to guilds.features --- manage/cmd/migration/scripts/12_remove_features_table.sql | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manage/cmd/migration/scripts/12_remove_features_table.sql b/manage/cmd/migration/scripts/12_remove_features_table.sql index 5f37056..7c8df16 100644 --- a/manage/cmd/migration/scripts/12_remove_features_table.sql +++ b/manage/cmd/migration/scripts/12_remove_features_table.sql @@ -1,4 +1,5 @@ DROP TABLE guild_features; DROP TABLE features; -ALTER TABLE guilds ADD COLUMN features text[]; +-- this should do the trick +ALTER TABLE guilds ADD COLUMN features text[] NOT NULL DEFAULT '{}'; From 16e583179c5f10020212c13a1d5678f0c05c0d2d Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 11 Mar 2019 02:12:09 -0300 Subject: [PATCH 08/35] schemas: fix coerce - run: change features_admin base to /admin/guilds --- litecord/schemas.py | 4 +++- run.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/litecord/schemas.py b/litecord/schemas.py index a746bfe..63c2c92 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -659,6 +659,8 @@ GET_MENTIONS = { FEATURES = { 'features': { 'type': 'list', 'required': True, - 'schema': {'coerce': Feature} + + # using Feature doesn't seem to work with a "not callable" error. + 'schema': {'coerce': lambda x: Feature(x)} } } diff --git a/run.py b/run.py index 8441f28..15ce2b4 100644 --- a/run.py +++ b/run.py @@ -145,7 +145,7 @@ def set_blueprints(app_): static: -1, voice_admin: '/admin/voice', - features_admin: '/admin/features', + features_admin: '/admin/guilds', guilds_admin: '/admin/guilds' } From 0dbfb3a210745287fdaf357b54c7d089d788e901 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 13 Mar 2019 04:09:43 -0300 Subject: [PATCH 09/35] admin_api.features: dispatch GUILD_UPDATE on feature changes - storage: fix typing on get_guild_full --- litecord/blueprints/admin_api/features.py | 3 +++ litecord/storage.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/litecord/blueprints/admin_api/features.py b/litecord/blueprints/admin_api/features.py index 7ee7a0c..980016a 100644 --- a/litecord/blueprints/admin_api/features.py +++ b/litecord/blueprints/admin_api/features.py @@ -45,6 +45,9 @@ async def _update_features(guild_id: int, features: list): WHERE id = $2 """, features, guild_id) + guild = await app.storage.get_guild_full(guild_id) + await app.dispatcher.dispatch('guild', guild_id, 'GUILD_UPDATE', guild) + @bp.route('//features', methods=['PATCH']) async def replace_features(guild_id: int): diff --git a/litecord/storage.py b/litecord/storage.py index d3b2257..2f4279b 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -633,7 +633,7 @@ class Storage: 'voice_states': await self.guild_voice_states(guild_id), }} - async def get_guild_full(self, guild_id: int, user_id: int, + async def get_guild_full(self, guild_id: int, user_id: Optional[int] = None, large_count: int = 250) -> Optional[Dict]: """Get full information on a guild. From 633cd730c0f20b1be9790447e47b6ac1783a66d1 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 13 Mar 2019 04:28:23 -0300 Subject: [PATCH 10/35] guilds: add basic vanity urls - add vanity_invites table --- litecord/blueprints/guilds.py | 20 +++++++++++++++++++ .../scripts/13_add_vanity_invites_table.sql | 5 +++++ schema.sql | 6 ++++++ 3 files changed, 31 insertions(+) create mode 100644 manage/cmd/migration/scripts/13_add_vanity_invites_table.sql diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index bed7881..47fe902 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -371,3 +371,23 @@ async def ack_guild(guild_id): await channel_ack(user_id, guild_id, chan_id) return '', 204 + + +@bp.route('//vanity-url', methods=['GET']) +async def get_vanity_url(guild_id: int): + """Get the vanity url of a guild.""" + user_id = await token_check() + + await guild_perm_check(user_id, guild_id, 'manage_guild') + + inv_code = await app.db.fetchval(""" + SELECT code FROM vanity_invites + WHERE guild_id = $1 + """, guild_id) + + if inv_code is None: + return jsonify({'code': None}) + + return jsonify( + await app.storage.get_invite(inv_code) + ) diff --git a/manage/cmd/migration/scripts/13_add_vanity_invites_table.sql b/manage/cmd/migration/scripts/13_add_vanity_invites_table.sql new file mode 100644 index 0000000..73aa914 --- /dev/null +++ b/manage/cmd/migration/scripts/13_add_vanity_invites_table.sql @@ -0,0 +1,5 @@ +-- vanity url table, the mapping is 1-1 for guilds and vanity urls +CREATE TABLE IF NOT EXISTS vanity_invites ( + guild_id bigint REFERENCES guilds (id) PRIMARY KEY, + code text REFERENCES invites (code) ON DELETE CASCADE +); diff --git a/schema.sql b/schema.sql index 0f5742d..32fa564 100644 --- a/schema.sql +++ b/schema.sql @@ -527,6 +527,12 @@ CREATE TABLE IF NOT EXISTS invites ( revoked bool DEFAULT false ); +-- vanity url table, the mapping is 1-1 for guilds and vanity urls +CREATE TABLE IF NOT EXISTS vanity_invites ( + guild_id bigint REFERENCES guilds (id) PRIMARY KEY, + code text REFERENCES invites (code) ON DELETE CASCADE +); + CREATE TABLE IF NOT EXISTS webhooks ( id bigint PRIMARY KEY, From f0d4c84e686596212b40382bdd2029c7cc4274a7 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 13 Mar 2019 04:48:25 -0300 Subject: [PATCH 11/35] guilds: add implementation for vanity invites I'm not proud of it, especially that we couldn't use much SQL power and things are laid off to Storage calls. It works, though - schemas: add VANITY_URL_PATCH --- litecord/blueprints/guilds.py | 79 ++++++++++++++++++++++++++++++++--- litecord/schemas.py | 5 +++ 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index 47fe902..91cbbef 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -17,6 +17,9 @@ along with this program. If not, see . """ +from typing import Optional + +from asyncpg import UniqueViolationError from quart import Blueprint, request, current_app as app, jsonify from litecord.blueprints.guild.channels import create_guild_channel @@ -28,7 +31,8 @@ from ..auth import token_check from ..snowflake import get_snowflake from ..enums import ChannelType from ..schemas import ( - validate, GUILD_CREATE, GUILD_UPDATE, SEARCH_CHANNEL + validate, GUILD_CREATE, GUILD_UPDATE, SEARCH_CHANNEL, + VANITY_URL_PATCH ) from .channels import channel_ack from .checks import guild_check, guild_owner_check, guild_perm_check @@ -373,17 +377,20 @@ async def ack_guild(guild_id): return '', 204 +async def _vanity_inv(guild_id) -> Optional[str]: + return await app.db.fetchval(""" + SELECT code FROM vanity_invites + WHERE guild_id = $1 + """, guild_id) + + @bp.route('//vanity-url', methods=['GET']) async def get_vanity_url(guild_id: int): """Get the vanity url of a guild.""" user_id = await token_check() - await guild_perm_check(user_id, guild_id, 'manage_guild') - inv_code = await app.db.fetchval(""" - SELECT code FROM vanity_invites - WHERE guild_id = $1 - """, guild_id) + inv_code = await _vanity_inv(guild_id) if inv_code is None: return jsonify({'code': None}) @@ -391,3 +398,63 @@ async def get_vanity_url(guild_id: int): return jsonify( await app.storage.get_invite(inv_code) ) + + +@bp.route('//vanity-url', methods=['PATCH']) +async def change_vanity_url(guild_id: int): + """Get the vanity url of a guild.""" + user_id = await token_check() + await guild_perm_check(user_id, guild_id, 'manage_guild') + + j = validate(await request.get_json(), VANITY_URL_PATCH) + inv_code = j['code'] + + # store old vanity in a variable to delete it from + # invites table + old_vanity = await _vanity_inv(guild_id) + + # this is sad because we don't really use the things + # sql gives us, but i havent really found a way to put + # multiple ON CONFLICT clauses so we could UPDATE when + # guild_id_fkey fails but INSERT when code_fkey fails.. + inv = await app.storage.get_invite(inv_code) + if inv: + raise BadRequest('invite already exists') + + # TODO: this is bad, what if a guild has no channels? + # we should probably choose the first channel that has + # @everyone read messages + channels = await app.storage.get_channel_data(guild_id) + channel_id = int(channels[0]['id']) + + # delete the old invite, insert new one + await app.db.execute(""" + DELETE FROM invites + WHERE code = $1 + """, old_vanity) + + await app.db.execute( + """ + INSERT INTO invites + (code, guild_id, channel_id, inviter, max_uses, + max_age, temporary) + VALUES ($1, $2, $3, $4, $5, $6, $7) + """, + inv_code, guild_id, channel_id, user_id, + + # sane defaults for vanity urls. + 0, 0, False, + ) + + await app.db.execute(""" + INSERT INTO vanity_invites (guild_id, code) + VALUES ($1, $2) + ON CONFLICT ON CONSTRAINT vanity_invites_pkey DO + UPDATE + SET code = $2 + WHERE vanity_invites.guild_id = $1 + """, guild_id, inv_code) + + return jsonify( + await app.storage.get_invite(inv_code) + ) diff --git a/litecord/schemas.py b/litecord/schemas.py index 63c2c92..9efe7a9 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -664,3 +664,8 @@ FEATURES = { 'schema': {'coerce': lambda x: Feature(x)} } } + +VANITY_URL_PATCH = { + # TODO: put proper values in maybe an invite data type + 'code': {'type': 'string', 'minlength': 5, 'maxlength': 30} +} From 5c5189480748ca138f4ff1c0d36dd44ad5777ff2 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 13 Mar 2019 04:49:50 -0300 Subject: [PATCH 12/35] guilds: add failsafe check for same invite --- litecord/blueprints/guilds.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index 91cbbef..ffcfc77 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -413,6 +413,9 @@ async def change_vanity_url(guild_id: int): # invites table old_vanity = await _vanity_inv(guild_id) + if old_vanity == inv_code: + raise BadRequest('can not change to same invite') + # this is sad because we don't really use the things # sql gives us, but i havent really found a way to put # multiple ON CONFLICT clauses so we could UPDATE when From afcdd145f4696f4ea86f38a3f8bea0e5ffa497c5 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 13 Mar 2019 05:03:26 -0300 Subject: [PATCH 13/35] emoji: add MORE_EMOJI feature support bumps emoji limit to 200 for both static and animated, needs more looking. - storage: add Storage.has_feature --- litecord/blueprints/guild/emoji.py | 30 +++++++++++++++++++++++++++++- litecord/blueprints/guilds.py | 1 - litecord/storage.py | 9 +++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/litecord/blueprints/guild/emoji.py b/litecord/blueprints/guild/emoji.py index 925adac..cc48dfe 100644 --- a/litecord/blueprints/guild/emoji.py +++ b/litecord/blueprints/guild/emoji.py @@ -24,6 +24,8 @@ 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 from litecord.types import KILOBYTES +from litecord.images import parse_data_uri +from litecord.errors import BadRequest bp = Blueprint('guild.emoji', __name__) @@ -54,6 +56,24 @@ async def _get_guild_emoji_one(guild_id, emoji_id): ) +async def _guild_emoji_size_check(guild_id: int, mime: str): + limit = 50 + if await app.storage.has_feature(guild_id, 'MORE_EMOJI'): + limit = 200 + + # NOTE: I'm assuming you can have 200 animated emojis. + select_animated = mime == 'image/gif' + + total_emoji = await app.db.fetchval(""" + SELECT COUNT(*) FROM guild_emoji + WHERE guild_id = $1 AND animated = $2 + """, guild_id, select_animated) + + if total_emoji >= limit: + # TODO: really return a BadRequest? needs more looking. + raise BadRequest(f'too many emoji ({limit})') + + @bp.route('//emojis', methods=['POST']) async def _put_emoji(guild_id): user_id = await token_check() @@ -63,6 +83,11 @@ async def _put_emoji(guild_id): j = validate(await request.get_json(), NEW_EMOJI) + # we have to parse it before passing on so that we know which + # size to check. + mime, _ = parse_data_uri(j['image']) + await _guild_emoji_size_check(guild_id, mime) + emoji_id = get_snowflake() icon = await app.icons.put( @@ -75,6 +100,8 @@ async def _put_emoji(guild_id): if not icon: return '', 400 + # TODO: better way to detect animated emoji rather than just gifs, + # maybe a list perhaps? await app.db.execute( """ INSERT INTO guild_emoji @@ -85,7 +112,8 @@ async def _put_emoji(guild_id): emoji_id, guild_id, user_id, j['name'], icon.icon_hash, - icon.mime == 'image/gif') + icon.mime == 'image/gif' + ) await _dispatch_emojis(guild_id) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index ffcfc77..8d2b669 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -19,7 +19,6 @@ along with this program. If not, see . from typing import Optional -from asyncpg import UniqueViolationError from quart import Blueprint, request, current_app as app, jsonify from litecord.blueprints.guild.channels import create_guild_channel diff --git a/litecord/storage.py b/litecord/storage.py index 2f4279b..04d9473 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -1076,3 +1076,12 @@ class Storage: """) return list(map(dict, rows)) + + async def has_feature(self, guild_id: int, feature: str) -> bool: + """Return if a certain guild has a certain feature.""" + features = await self.db.fetchval(""" + SELECT features FROM guilds + WHERE id = $1 + """, guild_id) + + return feature.upper() in features From 6763f2c501d40bbb1ad117f6c36d0ee33698e959 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 13 Mar 2019 05:04:48 -0300 Subject: [PATCH 14/35] guilds: add VANITY_URL feature check when editing vanity --- litecord/blueprints/guilds.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index 8d2b669..4c94953 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -403,6 +403,11 @@ async def get_vanity_url(guild_id: int): async def change_vanity_url(guild_id: int): """Get the vanity url of a guild.""" user_id = await token_check() + + if not await app.storage.has_feature(guild_id, 'VANITY_URL'): + # TODO: is this the right error + raise BadRequest('guild has no vanity url support') + await guild_perm_check(user_id, guild_id, 'manage_guild') j = validate(await request.get_json(), VANITY_URL_PATCH) From 977fa95ff206b55c83af1254dc8774ff3aa2ded8 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 13 Mar 2019 05:07:10 -0300 Subject: [PATCH 15/35] admin_api.features: delete vanity invites when guild loses feature - guilds: make _vanity_inv -> vanity_invite exportable function --- litecord/blueprints/admin_api/features.py | 14 ++++++++++++++ litecord/blueprints/guilds.py | 7 ++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/litecord/blueprints/admin_api/features.py b/litecord/blueprints/admin_api/features.py index 980016a..08898a6 100644 --- a/litecord/blueprints/admin_api/features.py +++ b/litecord/blueprints/admin_api/features.py @@ -23,6 +23,7 @@ from quart import Blueprint, current_app as app, jsonify, request from litecord.auth import admin_check from litecord.errors import BadRequest from litecord.schemas import validate, FEATURES +from litecord.blueprints.guilds import vanity_invite bp = Blueprint('features_admin', __name__) @@ -39,6 +40,19 @@ async def _features(guild_id: int): async def _update_features(guild_id: int, features: list): + if 'VANITY_URL' not in features: + existing_inv = await vanity_invite(guild_id) + + await app.db.execute(""" + DELETE FROM vanity_invites + WHERE guild_id = $1 + """, guild_id) + + await app.db.execute(""" + DELETE FROM invites + WHERE code = $1 + """, existing_inv) + await app.db.execute(""" UPDATE guilds SET features = $1 diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index 4c94953..29a479e 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -376,7 +376,8 @@ async def ack_guild(guild_id): return '', 204 -async def _vanity_inv(guild_id) -> Optional[str]: +async def vanity_invite(guild_id: int) -> Optional[str]: + """Get the vanity invite for a guild.""" return await app.db.fetchval(""" SELECT code FROM vanity_invites WHERE guild_id = $1 @@ -389,7 +390,7 @@ async def get_vanity_url(guild_id: int): user_id = await token_check() await guild_perm_check(user_id, guild_id, 'manage_guild') - inv_code = await _vanity_inv(guild_id) + inv_code = await vanity_invite(guild_id) if inv_code is None: return jsonify({'code': None}) @@ -415,7 +416,7 @@ async def change_vanity_url(guild_id: int): # store old vanity in a variable to delete it from # invites table - old_vanity = await _vanity_inv(guild_id) + old_vanity = await vanity_invite(guild_id) if old_vanity == inv_code: raise BadRequest('can not change to same invite') From a12c63a7bfa900cdd202e535488327b022a1203e Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 13 Mar 2019 18:28:48 -0300 Subject: [PATCH 16/35] add impl for VIP_REGIONS (untested) - lvsp_manager: add Region, and store vip region status in it - lvsp_manager: add LVSPManager.region --- litecord/blueprints/guilds.py | 6 ++++++ litecord/voice/lvsp_manager.py | 31 +++++++++++++++++++++++++------ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index 29a479e..af5c04f 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -223,6 +223,12 @@ async def _update_guild(guild_id): """, j['name'], guild_id) if 'region' in j: + is_vip = app.voice.lvsp.region(j['region']).vip + can_vip = await app.storage.has_feature(guild_id, 'VIP_REGIONS') + + if is_vip and not can_vip: + raise BadRequest('can not assign guild to vip-only region') + await app.db.execute(""" UPDATE guilds SET region = $1 diff --git a/litecord/voice/lvsp_manager.py b/litecord/voice/lvsp_manager.py index 78484e0..9a2eb41 100644 --- a/litecord/voice/lvsp_manager.py +++ b/litecord/voice/lvsp_manager.py @@ -19,6 +19,7 @@ along with this program. If not, see . from typing import Optional from collections import defaultdict +from dataclasses import dataclass from logbook import Logger @@ -26,6 +27,14 @@ from litecord.voice.lvsp_conn import LVSPConnection log = Logger(__name__) + +@dataclass +class Region: + """Voice region data.""" + id: str + vip: bool + + class LVSPManager: """Manager class for Litecord Voice Server Protocol (LVSP) connections. @@ -44,46 +53,52 @@ class LVSPManager: # maps Union[GuildID, DMId, GroupDMId] to server hostnames self.assign = {} + # quick storage for Region dataclass instances. + self._regions = {} + self.app.loop.create_task(self._spawn()) async def _spawn(self): """Spawn LVSPConnection for each region.""" regions = await self.app.db.fetch(""" - SELECT id + SELECT id, vip FROM voice_regions WHERE deprecated = false """) - regions = [r['id'] for r in regions] + regions = [Region(r['id'], r['vip']) for r in regions] if not regions: log.warning('no regions are setup') return for region in regions: + # store it locally for region() function + self._regions[region.id] = region + self.app.loop.create_task( self._spawn_region(region) ) - async def _spawn_region(self, region: str): + async def _spawn_region(self, region: Region): """Spawn a region. Involves fetching all the hostnames for the regions and spawning a LVSPConnection for each.""" servers = await self.app.db.fetch(""" SELECT hostname FROM voice_servers WHERE region_id = $1 - """, region) + """, region.id) if not servers: log.warning('region {} does not have servers', region) return servers = [r['hostname'] for r in servers] - self.servers[region] = servers + self.servers[region.id] = servers for hostname in servers: - conn = LVSPConnection(self, region, hostname) + conn = LVSPConnection(self, region.id, hostname) self.conns[hostname] = conn self.app.loop.create_task( @@ -144,3 +159,7 @@ class LVSPManager: async def assign_conn(self, key: int, hostname: str): """Assign a connection to a given key in the assign map""" self.assign[key] = hostname + + def region(self, region_id: str) -> Optional[Region]: + """Get a :class:`Region` instance:wq:wq""" + return self._regions.get(region_id) From 1ad328904d71ca5cc9b6470a40d9c4dafbdb2a4c Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 13 Mar 2019 18:35:01 -0300 Subject: [PATCH 17/35] admin_schemas: move FEATURES schema to there --- litecord/admin_schemas.py | 11 +++++++++++ litecord/blueprints/admin_api/features.py | 3 ++- litecord/schemas.py | 10 +--------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/litecord/admin_schemas.py b/litecord/admin_schemas.py index 3896dc9..18d0e50 100644 --- a/litecord/admin_schemas.py +++ b/litecord/admin_schemas.py @@ -17,6 +17,8 @@ along with this program. If not, see . """ +from litecord.enums import Feature + VOICE_SERVER = { 'hostname': {'type': 'string', 'maxlength': 255, 'required': True} } @@ -29,3 +31,12 @@ VOICE_REGION = { 'deprecated': {'type': 'boolean', 'default': False}, 'custom': {'type': 'boolean', 'default': False}, } + +FEATURES = { + 'features': { + 'type': 'list', 'required': True, + + # using Feature doesn't seem to work with a "not callable" error. + 'schema': {'coerce': lambda x: Feature(x)} + } +} diff --git a/litecord/blueprints/admin_api/features.py b/litecord/blueprints/admin_api/features.py index 08898a6..81d5099 100644 --- a/litecord/blueprints/admin_api/features.py +++ b/litecord/blueprints/admin_api/features.py @@ -22,7 +22,8 @@ from quart import Blueprint, current_app as app, jsonify, request from litecord.auth import admin_check from litecord.errors import BadRequest -from litecord.schemas import validate, FEATURES +from litecord.schemas import validate +from litecord.admin_schemas import FEATURES from litecord.blueprints.guilds import vanity_invite bp = Blueprint('features_admin', __name__) diff --git a/litecord/schemas.py b/litecord/schemas.py index 9efe7a9..80deca9 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -28,7 +28,7 @@ from .permissions import Permissions from .types import Color from .enums import ( ActivityType, StatusType, ExplicitFilter, RelationshipType, - MessageNotifications, ChannelType, VerificationLevel, Feature + MessageNotifications, ChannelType, VerificationLevel ) from litecord.embed.schemas import EMBED_OBJECT @@ -656,14 +656,6 @@ GET_MENTIONS = { 'guild_id': {'coerce': int, 'required': False} } -FEATURES = { - 'features': { - 'type': 'list', 'required': True, - - # using Feature doesn't seem to work with a "not callable" error. - 'schema': {'coerce': lambda x: Feature(x)} - } -} VANITY_URL_PATCH = { # TODO: put proper values in maybe an invite data type From 47db4802099ce3a727d5eb5dea3f80f31c046aa8 Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 13 Mar 2019 22:59:27 -0300 Subject: [PATCH 18/35] register: properly handle missing j.invite - schemas: fix 80col line breaks --- litecord/blueprints/auth.py | 20 ++++++++++++++------ litecord/schemas.py | 7 ++++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/litecord/blueprints/auth.py b/litecord/blueprints/auth.py index e779e27..e6d4124 100644 --- a/litecord/blueprints/auth.py +++ b/litecord/blueprints/auth.py @@ -23,6 +23,7 @@ import secrets import itsdangerous import bcrypt from quart import Blueprint, jsonify, request, current_app as app +from logbook import Logger from litecord.auth import token_check, create_user from litecord.schemas import validate, REGISTER, REGISTER_WITH_INVITE @@ -30,7 +31,7 @@ from litecord.errors import BadRequest from litecord.snowflake import get_snowflake from .invites import use_invite - +log = Logger(__name__) bp = Blueprint('auth', __name__) @@ -64,10 +65,17 @@ async def register(): j = await request.get_json() if not 'password' in j: - j['password'] = 'default_password' # we need some password to make a token + # we need a password to generate a token. + # passwords are optional, so + j['password'] = 'default_password' j = validate(j, REGISTER) - email, password, username, invite = j['email'] if 'email' in j else None, j['password'], j['username'], j['invite'] + + # they're optional + email = j.get('email') + invite = j.get('invite') + + username, password = j['username'], j['password'] new_id, pwd_hash = await create_user( username, email, password, app.db @@ -76,9 +84,9 @@ async def register(): if invite: try: await use_invite(new_id, invite) - except Exception as e: - print(e) - pass # do nothing + except Exception: + log.exception('failed to use invite for register {} {!r}', + new_id, invite) return jsonify({ 'token': make_token(new_id, pwd_hash) diff --git a/litecord/schemas.py b/litecord/schemas.py index 80deca9..97c0a8f 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -175,7 +175,12 @@ REGISTER = { 'username': {'type': 'username', 'required': True}, 'email': {'type': 'email', 'required': False}, 'password': {'type': 'string', 'minlength': 5, 'required': False}, - 'invite': {'type': 'string', 'required': False, 'nullable': True}, # optional + + # invite stands for a guild invite, not an instance invite (that's on + # the register_with_invite handler). + 'invite': {'type': 'string', 'required': False, 'nullable': True}, + + # following fields only sent by official client 'fingerprint': {'type': 'string', 'required': False, 'nullable': True}, # these are sent by official client 'captcha_key': {'type': 'string', 'required': False, 'nullable': True}, 'consent': {'type': 'boolean'}, From 5862dce92225c9df9b7d902cbebc483327c71c78 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 00:01:49 -0300 Subject: [PATCH 19/35] icons: add getters for splash and banner - fix channel-icons scope fetch --- litecord/blueprints/icons.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/litecord/blueprints/icons.py b/litecord/blueprints/icons.py index 65b8de2..c44f438 100644 --- a/litecord/blueprints/icons.py +++ b/litecord/blueprints/icons.py @@ -78,6 +78,18 @@ async def get_app_icon(application_id, icon_hash, ext): @bp.route('/channel-icons//', methods=['GET']) -async def _get_gdm_icon(guild_id: int, icon_file: str): +async def _get_gdm_icon(channel_id: int, icon_file: str): icon_hash, ext = splitext_(icon_file) - return await send_icon('channel-icons', guild_id, icon_hash, ext=ext) + return await send_icon('channel-icons', channel_id, icon_hash, ext=ext) + + +@bp.route('/splashes//', methods=['GET']) +async def _get_guild_splash(guild_id: int, icon_file: str): + icon_hash, ext = splitext_(icon_file) + return await send_icon('splash', guild_id, icon_hash, ext=ext) + + +@bp.route('/banners//', methods=['GET']) +async def _get_guild_banner(guild_id: int, icon_file: str): + icon_hash, ext = splitext_(icon_file) + return await send_icon('banner', guild_id, icon_hash, ext=ext) From c2c3ea30919d134d7f3e0b63271f1e49d5d3213d Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 00:08:41 -0300 Subject: [PATCH 20/35] icons: remove double get_guild_splash handler - icons: remove print debug --- litecord/blueprints/icons.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/litecord/blueprints/icons.py b/litecord/blueprints/icons.py index c44f438..69c1126 100644 --- a/litecord/blueprints/icons.py +++ b/litecord/blueprints/icons.py @@ -53,11 +53,6 @@ async def _get_guild_icon(guild_id: int, icon_file: str): return await send_icon('guild', guild_id, icon_hash, ext=ext) -@bp.route('/splashes//.', methods=['GET']) -async def _get_guild_splash(guild_id: int, splash_hash: str, ext: str): - pass - - @bp.route('/embed/avatars/.png') async def _get_default_user_avatar(discrim: int): pass @@ -66,7 +61,6 @@ async def _get_default_user_avatar(discrim: int): @bp.route('/avatars//') async def _get_user_avatar(user_id, avatar_file): size_int = int(request.args.get('size', '1024')) - print('user request size', size_int) avatar_hash, ext = splitext_(avatar_file) return await send_icon( 'user', user_id, avatar_hash, ext=ext) From 060e91907d1c113dbbb8a85b10e5ddeb976fcada Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 01:26:27 -0300 Subject: [PATCH 21/35] icons: properly remove unused vars --- litecord/blueprints/icons.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/litecord/blueprints/icons.py b/litecord/blueprints/icons.py index 69c1126..106e80e 100644 --- a/litecord/blueprints/icons.py +++ b/litecord/blueprints/icons.py @@ -18,7 +18,7 @@ along with this program. If not, see . """ from os.path import splitext -from quart import Blueprint, current_app as app, send_file, request +from quart import Blueprint, current_app as app, send_file bp = Blueprint('images', __name__) @@ -60,10 +60,8 @@ async def _get_default_user_avatar(discrim: int): @bp.route('/avatars//') async def _get_user_avatar(user_id, avatar_file): - size_int = int(request.args.get('size', '1024')) avatar_hash, ext = splitext_(avatar_file) - return await send_icon( - 'user', user_id, avatar_hash, ext=ext) + return await send_icon('user', user_id, avatar_hash, ext=ext) # @bp.route('/app-icons//.') From 1987db11e458c39194920ac770c1ed2745bb5a3d Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 02:41:30 -0300 Subject: [PATCH 22/35] guilds: decouple icon put logic into a func --- litecord/blueprints/guilds.py | 38 +++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index af5c04f..0d8df9f 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -108,17 +108,39 @@ async def guild_create_channels_prep(guild_id: int, channels: list): await create_guild_channel(guild_id, channel_id, ctype) -async def put_guild_icon(guild_id: int, icon: str): - """Insert a guild icon on the icon database.""" +def sanitize_icon(icon: Optional[str]) -> Optional[str]: + """Return sanitized version of the given icon. + + Defaults to a jpeg icon when the header isn't given. + """ if icon and icon.startswith('data'): - encoded = icon - else: - encoded = (f'data:image/jpeg;base64,{icon}' - if icon - else None) + return icon + + return (f'data:image/jpeg;base64,{icon}' + if icon + else None) + + +async def _general_guild_icon(scope: str, guild_id: int, + icon: str, **kwargs): + encoded = sanitize_icon(icon) + + icon_kwargs = { + 'always_icon': True + } + + if 'size' in kwargs: + icon_kwargs['size'] = kwargs['size'] return await app.icons.put( - 'guild', guild_id, encoded, size=(128, 128), always_icon=True) + scope, guild_id, encoded, + **icon_kwargs + ) + + +async def put_guild_icon(guild_id: int, icon: Optional[str]): + """Insert a guild icon on the icon database.""" + return await _general_guild_icon('guild', guild_id, icon, size=(128, 128)) @bp.route('', methods=['POST']) From 60ccd8db0b2ad4ad93302b2275d9d49226535f72 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 03:16:17 -0300 Subject: [PATCH 23/35] guilds: add size=(128, 128) when updating guild icon --- litecord/blueprints/guilds.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index 0d8df9f..3ab0ce6 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -260,7 +260,7 @@ async def _update_guild(guild_id): if 'icon' in j: # delete old new_icon = await app.icons.update( - 'guild', guild_id, j['icon'], always_icon=True + 'guild', guild_id, j['icon'], always_icon=True, size=(128, 128) ) await app.db.execute(""" From 79735eedf826499e16d7479c9a8a447e457e0be6 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 03:33:51 -0300 Subject: [PATCH 24/35] guilds: add (untested) impl for splash and banner this finishes all theoretical implementations for features. Closes #36 and #37. --- litecord/blueprints/guilds.py | 40 +++++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index 3ab0ce6..bdb8b27 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -220,6 +220,24 @@ async def get_guild(guild_id): ) +async def _guild_update_icon(scope: str, guild_id: int, + icon: Optional[str], **kwargs): + """Update icon.""" + new_icon = await app.icons.update( + 'guild', guild_id, icon, always_icon=True, **kwargs + ) + + table = { + 'guild': 'icon', + }.get(scope, scope) + + await app.db.execute(f""" + UPDATE guilds + SET {table} = $1 + WHERE id = $2 + """, new_icon.icon_hash, guild_id) + + @bp.route('/', methods=['PATCH']) async def _update_guild(guild_id): user_id = await token_check() @@ -258,16 +276,20 @@ async def _update_guild(guild_id): """, j['region'], guild_id) if 'icon' in j: - # delete old - new_icon = await app.icons.update( - 'guild', guild_id, j['icon'], always_icon=True, size=(128, 128) - ) + await _guild_update_icon( + 'guild', guild_id, j['splash'], size=(128, 128)) - await app.db.execute(""" - UPDATE guilds - SET icon = $1 - WHERE id = $2 - """, new_icon.icon_hash, guild_id) + if 'splash' in j: + if not await app.storage.has_feature(guild_id, 'INVITE_SPLASH'): + raise BadRequest('guild does not have INVITE_SPLASH feature') + + await _guild_update_icon('splash', guild_id, j['splash']) + + if 'banner' in j: + if not await app.storage.has_feature(guild_id, 'VERIFIED'): + raise BadRequest('guild is not verified') + + await _guild_update_icon('banner', guild_id, j['banner']) fields = ['verification_level', 'default_message_notifications', 'explicit_content_filter', 'afk_timeout'] From 448b74f91beea61b27be8c8a7aad722992b39332 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 03:41:51 -0300 Subject: [PATCH 25/35] schemas: add GUILD_UPDATE.{banner, description} - storage: add banner and description to get_guild fetch - migration: add 14_add_guild_description.sql --- litecord/schemas.py | 8 ++++++++ litecord/storage.py | 3 ++- manage/cmd/migration/scripts/14_add_guild_description.sql | 2 ++ 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 manage/cmd/migration/scripts/14_add_guild_description.sql diff --git a/litecord/schemas.py b/litecord/schemas.py index 75424c1..893d46e 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -278,8 +278,16 @@ GUILD_UPDATE = { 'required': False }, 'region': {'type': 'voice_region', 'required': False, 'nullable': True}, + 'icon': {'type': 'b64_icon', 'required': False, 'nullable': True}, 'splash': {'type': 'b64_icon', 'required': False, 'nullable': True}, + 'banner': {'type': 'b64_icon', 'required': False, 'nullable': True}, + + 'description': { + 'type': 'string', 'required': False, + 'minlength': 1, 'maxlength': 120, + 'nullable': True + }, 'verification_level': { 'type': 'verification_level', 'required': False}, diff --git a/litecord/storage.py b/litecord/storage.py index 04d9473..2054d56 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -162,7 +162,8 @@ class Storage: explicit_content_filter, mfa_level, embed_enabled, embed_channel_id::text, widget_enabled, widget_channel_id::text, - system_channel_id::text, features + system_channel_id::text, features, + banner, description FROM guilds WHERE guilds.id = $1 """, guild_id) diff --git a/manage/cmd/migration/scripts/14_add_guild_description.sql b/manage/cmd/migration/scripts/14_add_guild_description.sql new file mode 100644 index 0000000..c9dbecb --- /dev/null +++ b/manage/cmd/migration/scripts/14_add_guild_description.sql @@ -0,0 +1,2 @@ +ALTER TABLE guilds ADD COLUMN description text DEFAULT NULL; +ALTER TABLE guilds ADD COLUMN banner text DEFAULT NULL; From 2bc3817a4ec762d49946eff2e372ffa14bf197c5 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 03:46:12 -0300 Subject: [PATCH 26/35] guilds: fix region update vip check --- litecord/blueprints/guilds.py | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index bdb8b27..0a98676 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -238,6 +238,21 @@ async def _guild_update_icon(scope: str, guild_id: int, """, new_icon.icon_hash, guild_id) +async def _guild_update_region(guild_id, region): + is_vip = region.vip + can_vip = await app.storage.has_feature(guild_id, 'VIP_REGIONS') + + if is_vip and not can_vip: + raise BadRequest('can not assign guild to vip-only region') + + await app.db.execute(""" + UPDATE guilds + SET region = $1 + WHERE id = $2 + """, region.id, guild_id) + + + @bp.route('/', methods=['PATCH']) async def _update_guild(guild_id): user_id = await token_check() @@ -263,17 +278,10 @@ async def _update_guild(guild_id): """, j['name'], guild_id) if 'region' in j: - is_vip = app.voice.lvsp.region(j['region']).vip - can_vip = await app.storage.has_feature(guild_id, 'VIP_REGIONS') + region = app.voice.lvsp.region(j['region']) - if is_vip and not can_vip: - raise BadRequest('can not assign guild to vip-only region') - - await app.db.execute(""" - UPDATE guilds - SET region = $1 - WHERE id = $2 - """, j['region'], guild_id) + if region is not None: + await _guild_update_region(guild_id, region) if 'icon' in j: await _guild_update_icon( From 7cd95d3f94a5b92281494868bb0684f697b946e6 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 03:54:40 -0300 Subject: [PATCH 27/35] gateway.websocket: fix ETF encoding with a json pass - icons: fix get guild banner handler args --- litecord/blueprints/icons.py | 2 +- litecord/gateway/websocket.py | 11 ++++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/litecord/blueprints/icons.py b/litecord/blueprints/icons.py index 106e80e..7434bad 100644 --- a/litecord/blueprints/icons.py +++ b/litecord/blueprints/icons.py @@ -81,7 +81,7 @@ async def _get_guild_splash(guild_id: int, icon_file: str): return await send_icon('splash', guild_id, icon_hash, ext=ext) -@bp.route('/banners//', methods=['GET']) +@bp.route('/banners//', methods=['GET']) async def _get_guild_banner(guild_id: int, icon_file: str): icon_hash, ext = splitext_(icon_file) return await send_icon('banner', guild_id, icon_hash, ext=ext) diff --git a/litecord/gateway/websocket.py b/litecord/gateway/websocket.py index c2977b7..4cc22aa 100644 --- a/litecord/gateway/websocket.py +++ b/litecord/gateway/websocket.py @@ -74,7 +74,16 @@ def decode_json(data: str): def encode_etf(payload) -> str: - return earl.pack(payload) + # The thing with encoding ETF is that with json we have LitecordJSONEncoder + # which takes care of converting e.g datetime objects to their ISO + # representation. + + # so, to keep things working, i'll to a json pass on the payload, then send + # the decoded payload back to earl. + + sanitized = encode_json(payload) + sanitized = decode_json(sanitized) + return earl.pack(sanitized) def _etf_decode_dict(data): From 44b81ea2f6a4e6c86407129637b789c5e0cf40ba Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 04:17:57 -0300 Subject: [PATCH 28/35] guilds: fix guild update icon call - images: add support for splash and banner - add jpeg check when converting extension --- litecord/blueprints/guilds.py | 2 +- litecord/images.py | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index 0a98676..4e11755 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -224,7 +224,7 @@ async def _guild_update_icon(scope: str, guild_id: int, icon: Optional[str], **kwargs): """Update icon.""" new_icon = await app.icons.update( - 'guild', guild_id, icon, always_icon=True, **kwargs + scope, guild_id, icon, always_icon=True, **kwargs ) table = { diff --git a/litecord/images.py b/litecord/images.py index 1db1ece..8db1e2c 100644 --- a/litecord/images.py +++ b/litecord/images.py @@ -46,6 +46,7 @@ EXTENSIONS = { MIMES = { 'jpg': 'image/jpeg', 'jpe': 'image/jpeg', + 'jpeg': 'image/jpeg', 'webp': 'image/webp', } @@ -171,15 +172,23 @@ def parse_data_uri(string) -> tuple: def _gen_update_sql(scope: str) -> str: + # match a scope to (table, field) field = { 'user': 'avatar', 'guild': 'icon', + 'splash': 'splash', + 'banner': 'banner', + 'channel-icons': 'icon', }[scope] table = { 'user': 'users', + 'guild': 'guilds', + 'splash': 'guilds', + 'banner': 'guilds', + 'channel-icons': 'group_dm_channels' }[scope] @@ -269,6 +278,8 @@ class IconManager: self.storage = app.storage async def _convert_ext(self, icon: Icon, target: str): + target = 'jpeg' if target == 'jpg' else target + target_mime = get_mime(target) log.info('converting from {} to {}', icon.mime, target_mime) @@ -279,6 +290,10 @@ class IconManager: image = Image.open(icon.as_path) target_fd = target_path.open('wb') + + if target == 'jpeg': + image = image.convert('RGB') + image.save(target_fd, format=target) target_fd.close() @@ -386,6 +401,9 @@ class IconManager: if scope == 'user' and mime == 'image/gif': icon_hash = f'a_{icon_hash}' + log.debug('PUT icon {!r} {!r} {!r} {!r}', + scope, key, icon_hash, mime) + await self.storage.db.execute(""" INSERT INTO icons (scope, key, hash, mime) VALUES ($1, $2, $3, $4) @@ -406,6 +424,9 @@ class IconManager: if not icon: return + log.debug('DEL {}', + icon) + # dereference await self.storage.db.execute(""" UPDATE users @@ -430,6 +451,18 @@ class IconManager: WHERE icon = $1 """, icon.icon_hash) + await self.storage.db.execute(""" + UPDATE guilds + SET splash = NULL + WHERE splash = $1 + """, icon.icon_hash) + + await self.storage.db.execute(""" + UPDATE guilds + SET banner = NULL + WHERE banner = $1 + """, icon.icon_hash) + await self.storage.db.execute(""" UPDATE group_dm_channels SET icon = NULL @@ -455,7 +488,11 @@ class IconManager: old_icon_hash = await self.storage.db.fetchval( _gen_update_sql(scope), key) + # converting key to str only here since from here onwards + # its operations on the icons table (or a dereference with + # the delete() method but that will work regardless) key = str(key) + old_icon = await self.generic_get(scope, key, old_icon_hash) await self.delete(old_icon) From b6e21a2501c472712b872a624b35147f25057c35 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 04:29:35 -0300 Subject: [PATCH 29/35] guilds: handle banner changes like user avatar changes just like user avatars, the client can just send the icon hash, we need to check that first beforehand. - guilds: add description update - utils: move to_update() from blueprints.users to there --- litecord/blueprints/guilds.py | 8 ++++++-- litecord/blueprints/users.py | 7 ++----- litecord/schemas.py | 5 ++++- litecord/utils.py | 5 +++++ 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index 4e11755..ebdca11 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -35,6 +35,7 @@ from ..schemas import ( ) from .channels import channel_ack from .checks import guild_check, guild_owner_check, guild_perm_check +from litecord.utils import to_update from litecord.errors import BadRequest @@ -293,14 +294,17 @@ async def _update_guild(guild_id): await _guild_update_icon('splash', guild_id, j['splash']) - if 'banner' in j: + # small guild to work with to_update() + guild = await app.storage.get_guild(guild_id) + + if to_update(j, guild, 'banner'): if not await app.storage.has_feature(guild_id, 'VERIFIED'): raise BadRequest('guild is not verified') await _guild_update_icon('banner', guild_id, j['banner']) fields = ['verification_level', 'default_message_notifications', - 'explicit_content_filter', 'afk_timeout'] + 'explicit_content_filter', 'afk_timeout', 'description'] for field in [f for f in fields if f in j]: await app.db.execute(f""" diff --git a/litecord/blueprints/users.py b/litecord/blueprints/users.py index 261b0a7..eff1be9 100644 --- a/litecord/blueprints/users.py +++ b/litecord/blueprints/users.py @@ -32,10 +32,11 @@ from litecord.auth import token_check, hash_data, check_username_usage from litecord.blueprints.guild.mod import remove_member from litecord.enums import PremiumType -from litecord.images import parse_data_uri +from litecord.images import parse_data_uri, ImageError from litecord.permissions import base_permissions from litecord.blueprints.auth import check_password +from litecord.utils import to_update bp = Blueprint('user', __name__) log = Logger(__name__) @@ -185,10 +186,6 @@ async def _try_discrim_patch(user_id, new_discrim: str): }) -def to_update(j: dict, user: dict, field: str): - return field in j and j[field] and j[field] != user[field] - - async def _check_pass(j, user): # Do not do password checks on unclaimed accounts if user['email'] is None: diff --git a/litecord/schemas.py b/litecord/schemas.py index 893d46e..6796ed8 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -281,7 +281,10 @@ GUILD_UPDATE = { 'icon': {'type': 'b64_icon', 'required': False, 'nullable': True}, 'splash': {'type': 'b64_icon', 'required': False, 'nullable': True}, - 'banner': {'type': 'b64_icon', 'required': False, 'nullable': True}, + + # TODO: does splash also respect when its just a string pointing to the + # hash, just like in USER_UPDATE.avatar? + 'banner': {'type': 'string', 'required': False, 'nullable': True}, 'description': { 'type': 'string', 'required': False, diff --git a/litecord/utils.py b/litecord/utils.py index 513fafa..d19df06 100644 --- a/litecord/utils.py +++ b/litecord/utils.py @@ -174,3 +174,8 @@ def yield_chunks(input_list: Sequence[Any], chunk_size: int): # make the chunks for idx in range(0, len(input_list), chunk_size): yield input_list[idx:idx + chunk_size] + +def to_update(j: dict, orig: dict, field: str) -> bool: + """Compare values to check if j[field] is actually updating + the value in orig[field]. Useful for icon checks.""" + return field in j and j[field] and j[field] != orig[field] From 5bfafa2a2961abf8741a21733c9657f9822d22df Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 04:32:02 -0300 Subject: [PATCH 30/35] storage: expose banner and description on invites --- litecord/storage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/litecord/storage.py b/litecord/storage.py index 2054d56..72812a7 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -919,7 +919,8 @@ class Storage: # fetch some guild info guild = await self.db.fetchrow(""" - SELECT id::text, name, splash, icon, verification_level + SELECT id::text, name, icon, splash, banner, features, + verification_level, description FROM guilds WHERE id = $1 """, invite['guild_id']) From 5c0dfcc9b1a636de3cd625b0b863653f6524c501 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 04:39:09 -0300 Subject: [PATCH 31/35] guilds: add support for icon hash for j.splash - guilds: fix guild icon being set to j.splash - schemas: handle splash being icon hash --- litecord/blueprints/guilds.py | 10 +++++----- litecord/schemas.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/litecord/blueprints/guilds.py b/litecord/blueprints/guilds.py index ebdca11..c4675b8 100644 --- a/litecord/blueprints/guilds.py +++ b/litecord/blueprints/guilds.py @@ -286,17 +286,17 @@ async def _update_guild(guild_id): if 'icon' in j: await _guild_update_icon( - 'guild', guild_id, j['splash'], size=(128, 128)) + 'guild', guild_id, j['icon'], size=(128, 128)) - if 'splash' in j: + # small guild to work with to_update() + guild = await app.storage.get_guild(guild_id) + + if to_update(j, guild, 'splash'): if not await app.storage.has_feature(guild_id, 'INVITE_SPLASH'): raise BadRequest('guild does not have INVITE_SPLASH feature') await _guild_update_icon('splash', guild_id, j['splash']) - # small guild to work with to_update() - guild = await app.storage.get_guild(guild_id) - if to_update(j, guild, 'banner'): if not await app.storage.has_feature(guild_id, 'VERIFIED'): raise BadRequest('guild is not verified') diff --git a/litecord/schemas.py b/litecord/schemas.py index 6796ed8..c52cf8f 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -280,11 +280,11 @@ GUILD_UPDATE = { 'region': {'type': 'voice_region', 'required': False, 'nullable': True}, 'icon': {'type': 'b64_icon', 'required': False, 'nullable': True}, - 'splash': {'type': 'b64_icon', 'required': False, 'nullable': True}, # TODO: does splash also respect when its just a string pointing to the # hash, just like in USER_UPDATE.avatar? 'banner': {'type': 'string', 'required': False, 'nullable': True}, + 'splash': {'type': 'string', 'required': False, 'nullable': True}, 'description': { 'type': 'string', 'required': False, From b90089a7846c9620ec837a37f630c75ed0e00c06 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 04:43:26 -0300 Subject: [PATCH 32/35] users: remove unused import --- litecord/blueprints/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litecord/blueprints/users.py b/litecord/blueprints/users.py index eff1be9..1145b8d 100644 --- a/litecord/blueprints/users.py +++ b/litecord/blueprints/users.py @@ -32,7 +32,7 @@ from litecord.auth import token_check, hash_data, check_username_usage from litecord.blueprints.guild.mod import remove_member from litecord.enums import PremiumType -from litecord.images import parse_data_uri, ImageError +from litecord.images import parse_data_uri from litecord.permissions import base_permissions from litecord.blueprints.auth import check_password From 30be092c3cea8dd613e61748bb74af42c9fea645 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 16:09:52 -0300 Subject: [PATCH 33/35] docs/admin_api.md: update headings --- docs/admin_api.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/admin_api.md b/docs/admin_api.md index 3c4649c..7e0ea31 100644 --- a/docs/admin_api.md +++ b/docs/admin_api.md @@ -2,7 +2,9 @@ the base path is `/api/v6/admin`. -## GET `/voice/regions/` +## Voice + +### GET `/voice/regions/` Return a list of voice server objects for the region. @@ -13,7 +15,7 @@ Returns empty list if the region does not exist. | hostname | string | the hostname of the voice server | | last\_health | float | the health of the voice server | -## PUT `/voice/regions` +### PUT `/voice/regions` Create a voice region. @@ -27,7 +29,7 @@ Receives JSON body as input, returns a list of voice region objects as output. | deprecated | Optional[bool] | if voice region is deprecated, default false | | custom | Optional[bool] | if voice region is custom-only, default false | -## PUT `/voice/regions//server` +### PUT `/voice/regions//server` Create a voice server for a region. @@ -37,7 +39,7 @@ Returns empty body with 204 status code on success. | --: | :-- | :-- | | hostname | string | the hostname of the voice server | -## PUT `/voice/regions//deprecate` +### PUT `/voice/regions//deprecate` Mark a voice region as deprecated. Disables any voice actions on guilds that are using the voice region. From 276fc4c0938f2422e5e8a4b4e447e61b07b07877 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 16:14:44 -0300 Subject: [PATCH 34/35] docs/admin_api.md: add Guilds and Guild features sections --- docs/admin_api.md | 27 +++++++++++++++++++++++ litecord/blueprints/admin_api/features.py | 4 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/docs/admin_api.md b/docs/admin_api.md index 7e0ea31..c2724a6 100644 --- a/docs/admin_api.md +++ b/docs/admin_api.md @@ -45,3 +45,30 @@ Mark a voice region as deprecated. Disables any voice actions on guilds that are using the voice region. Returns empty body with 204 status code on success. + +## Guilds + +### GET `/guilds/` + +Returns a partial guild object. + +## Guild features + +### PATCH `/guilds//features` + +Patch the entire features list. Returns the new feature list following the same +structure as the input. + +| field | type | description | +| --: | :-- | :-- | +| features | List[string] | new list of features | + +### PUT `/guilds//features` + +Insert features. Receives and returns the same structure as +PATCH `/guilds//features`. + +### DELETE `/guilds//features` + +Remove features. Receives and returns the same structure as +PATCH `/guilds//features`. diff --git a/litecord/blueprints/admin_api/features.py b/litecord/blueprints/admin_api/features.py index 81d5099..69b338f 100644 --- a/litecord/blueprints/admin_api/features.py +++ b/litecord/blueprints/admin_api/features.py @@ -94,8 +94,8 @@ async def insert_features(guild_id: int): return await _features(guild_id) -@bp.route('//', methods=['DELETE']) -async def remove_feature(guild_id: int): +@bp.route('//features', methods=['DELETE']) +async def remove_features(guild_id: int): """Remove a feature from a guild""" await admin_check() to_remove = await _features_from_req() From e8c55ac2a983441e6257aa05bb13bf2af5e235b3 Mon Sep 17 00:00:00 2001 From: Luna Date: Fri, 15 Mar 2019 16:17:18 -0300 Subject: [PATCH 35/35] docs/admin_api.md: add supported features --- docs/admin_api.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/admin_api.md b/docs/admin_api.md index c2724a6..4dc8863 100644 --- a/docs/admin_api.md +++ b/docs/admin_api.md @@ -54,6 +54,19 @@ Returns a partial guild object. ## Guild features +The currently supported features are: + - `INVITE_SPLASH`, allows custom images to be put for invites. + - `VIP_REGIONS`, allows a guild to use voice regions marked as VIP. + - `VANITY_URL`, allows a custom invite URL to be used. + - `MORE_EMOJI`, bumps the emoji limit from 50 to 200 (applies to static and + animated emoji). + - `VERIFIED`, adds a verified badge and a guild banner being shown on the + top of the channel list. + +Features that are not planned to be implemented: + - `COMMERCE` + - `NEWS` + ### PATCH `/guilds//features` Patch the entire features list. Returns the new feature list following the same