diff --git a/litecord/blueprints/channel/messages.py b/litecord/blueprints/channel/messages.py index 5bdd925..4bd0206 100644 --- a/litecord/blueprints/channel/messages.py +++ b/litecord/blueprints/channel/messages.py @@ -139,8 +139,88 @@ async def _dm_pre_dispatch(channel_id, peer_id): await try_dm_state(peer_id, channel_id) +async def create_message(channel_id: int, actual_guild_id: int, + author_id: int, data: dict) -> int: + message_id = get_snowflake() + + await app.db.execute( + """ + INSERT INTO messages (id, channel_id, guild_id, author_id, + content, tts, mention_everyone, nonce, message_type) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + """, + message_id, + channel_id, + actual_guild_id, + author_id, + data['content'], + + data['tts'], + data['everyone_mention'], + + data['nonce'], + MessageType.DEFAULT.value + ) + + return message_id + +async def _guild_text_mentions(payload: dict, guild_id: int, + mentions_everyone: bool, mentions_here: bool): + channel_id = int(payload['channel_id']) + + # calculate the user ids we'll bump the mention count for + uids = set() + + # first is extracting user mentions + for mention in payload['mentions']: + uids.add(int(mention['id'])) + + # then role mentions + for role_mention in payload['mention_roles']: + role_id = int(role_mention) + member_ids = await app.storage.get_role_members(role_id) + + for member_id in member_ids: + uids.add(member_id) + + # at-here only updates the state + # for the users that have a state + # in the channel. + if mentions_here: + uids = [] + await app.db.execute(""" + UPDATE user_read_state + SET mention_count = mention_count + 1 + WHERE channel_id = $1 + """, channel_id) + + # at-here updates the read state + # for all users, including the ones + # that might not have read permissions + # to the channel. + if mentions_everyone: + uids = [] + + member_ids = await app.storage.get_member_ids(guild_id) + + await app.db.executemany(""" + UPDATE user_read_state + SET mention_count = mention_count + 1 + WHERE channel_id = $1 AND user_id = $2 + """, [(channel_id, uid) for uid in member_ids]) + + for user_id in uids: + await app.db.execute(""" + UPDATE user_read_state + SET mention_count = mention_count + 1 + WHERE user_id = $1 + AND channel_id = $2 + """, user_id, channel_id) + + @bp.route('//messages', methods=['POST']) -async def create_message(channel_id): +async def _create_message(channel_id): + """Create a message.""" user_id = await token_check() ctype, guild_id = await channel_check(user_id, channel_id) @@ -167,24 +247,12 @@ async def create_message(channel_id): user_id, channel_id, 'send_tts_messages', False )) - await app.db.execute( - """ - INSERT INTO messages (id, channel_id, guild_id, author_id, - content, tts, mention_everyone, nonce, message_type) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) - """, - message_id, - channel_id, - actual_guild_id, - user_id, - j['content'], - - is_tts, - mentions_everyone or mentions_here, - - int(j.get('nonce', 0)), - MessageType.DEFAULT.value - ) + await create_message(channel_id, actual_guild_id, user_id, { + 'content': j['content'], + 'tts': is_tts, + 'nonce': int(j.get('nonce', 0)), + 'everyone_mention': mentions_everyone or mentions_here, + }) payload = await app.storage.get_message(message_id, user_id) @@ -196,6 +264,7 @@ async def create_message(channel_id): await app.dispatcher.dispatch('channel', channel_id, 'MESSAGE_CREATE', payload) + # update read state for the author await app.db.execute(""" UPDATE user_read_state SET last_message_id = $1 @@ -203,54 +272,8 @@ async def create_message(channel_id): """, message_id, channel_id, user_id) if ctype == ChannelType.GUILD_TEXT: - # calculate the user ids we'll bump the mention count for - uids = set() - - # first is extracting user mentions - for mention in payload['mentions']: - uids.add(int(mention['id'])) - - # then role mentions - for role_mention in payload['mention_roles']: - role_id = int(role_mention) - member_ids = await app.storage.get_role_members(role_id) - - for member_id in member_ids: - uids.add(member_id) - - # at-here only updates the state - # for the users that have a state - # in the channel. - if mentions_here: - uids = [] - await app.db.execute(""" - UPDATE user_read_state - SET mention_count = mention_count + 1 - WHERE channel_id = $1 - """, channel_id) - - # at-here updates the read state - # for all users, including the ones - # that might not have read permissions - # to the channel. - if mentions_everyone: - uids = [] - - member_ids = await app.storage.get_member_ids(guild_id) - - await app.db.executemany(""" - UPDATE user_read_state - SET mention_count = mention_count + 1 - WHERE channel_id = $1 AND user_id = $2 - """, [(channel_id, uid) for uid in member_ids]) - - for user_id in uids: - await app.db.execute(""" - UPDATE user_read_state - SET mention_count = mention_count + 1 - WHERE user_id = $1 - AND channel_id = $2 - """, user_id, channel_id) + await _guild_text_mentions(payload, guild_id, + mentions_everyone, mentions_here) return jsonify(payload) diff --git a/litecord/embed/schemas.py b/litecord/embed/schemas.py new file mode 100644 index 0000000..1d80867 --- /dev/null +++ b/litecord/embed/schemas.py @@ -0,0 +1,108 @@ +""" +litecord.embed.schemas - embed input validators. +""" +import urllib.parse + +from litecord.types import Color + + +class EmbedURL: + def __init__(self, url: str): + parsed = urllib.parse.urlparse(url) + + if parsed.scheme not in ('http', 'https', 'attachment'): + raise ValueError('Invalid URL scheme') + + self.raw_url = url + self.parsed = parsed + + @property + def url(self): + """Return the URL.""" + return urllib.parse.urlunparse(self.parsed) + + +EMBED_FOOTER = { + 'text': { + 'type': 'string', 'minlength': 1, 'maxlength': 128, 'required': True}, + + 'icon_url': { + 'coerce': EmbedURL, 'required': False, + }, + + # NOTE: proxy_icon_url set by us +} + +EMBED_IMAGE = { + 'url': {'coerce': EmbedURL, 'required': True}, + + # NOTE: proxy_url, width, height set by us +} + +EMBED_THUMBNAIL = EMBED_IMAGE + +EMBED_AUTHOR = { + 'name': { + 'type': 'string', 'minlength': 1, 'maxlength': 128, 'required': False + }, + 'url': { + 'type': EmbedURL, 'required': False, + }, + 'icon_url': { + 'coerce': EmbedURL, 'required': False, + } +} + +EMBED_OBJECT = { + 'title': { + 'type': 'string', 'minlength': 1, 'maxlength': 128, 'required': False}, + 'type': { + 'type': 'string', 'allowed': ['rich'], 'required': False, + 'default': 'rich' + }, + 'description': { + 'type': 'string', 'minlength': 1, 'maxlength': 1024, 'required': False, + }, + 'url': { + 'coerce': EmbedURL, 'required': False, + }, + 'timestamp': { + # TODO: an ISO 8601 type + # TODO: maybe replace the default in here with now().isoformat? + 'type': 'string', 'required': False + }, + + 'color': { + 'coerce': Color, 'required': False + }, + + 'footer': { + 'type': 'dict', + 'schema': EMBED_FOOTER, + 'required': False, + }, + 'image': { + 'type': 'dict', + 'schema': EMBED_IMAGE, + 'required': False, + }, + 'thumbnail': { + 'type': 'dict', + 'schema': EMBED_THUMBNAIL, + 'required': False, + }, + + # NOTE: 'video' set by us + # NOTE: 'provider' set by us + + 'author': { + 'type': 'dict', + 'schema': EMBED_AUTHOR, + 'required': False, + }, + 'fields': { + 'type': 'list', + 'schema': EMBED_AUTHOR, + 'required': False, + }, +}