diff --git a/litecord/blueprints/channel/messages.py b/litecord/blueprints/channel/messages.py index 2fdd904..2e15825 100644 --- a/litecord/blueprints/channel/messages.py +++ b/litecord/blueprints/channel/messages.py @@ -194,8 +194,9 @@ async def create_message(channel_id: int, actual_guild_id: int, return message_id -async def _guild_text_mentions(payload: dict, guild_id: int, - mentions_everyone: bool, mentions_here: bool): +async def msg_guild_text_mentions(payload: dict, guild_id: int, + mentions_everyone: bool, mentions_here: bool): + """Calculates mention data side-effects.""" channel_id = int(payload['channel_id']) # calculate the user ids we'll bump the mention count for @@ -435,8 +436,8 @@ async def _create_message(channel_id): """, message_id, channel_id, user_id) if ctype == ChannelType.GUILD_TEXT: - await _guild_text_mentions(payload, guild_id, - mentions_everyone, mentions_here) + await msg_guild_text_mentions( + payload, guild_id, mentions_everyone, mentions_here) return jsonify(payload) diff --git a/litecord/blueprints/webhooks.py b/litecord/blueprints/webhooks.py index 61670d0..30b9c23 100644 --- a/litecord/blueprints/webhooks.py +++ b/litecord/blueprints/webhooks.py @@ -37,10 +37,12 @@ from litecord.errors import WebhookNotFound, Unauthorized, ChannelNotFound from litecord.blueprints.channel.messages import ( msg_create_request, msg_create_check_content, msg_add_attachment, - # create_message + msg_guild_text_mentions ) from litecord.embed.sanitizer import fill_embed from litecord.embed.messages import process_url_embed +from litecord.utils import pg_set_json +from litecord.enums import MessageType bp = Blueprint('webhooks', __name__) @@ -304,9 +306,34 @@ async def del_webhook_tokened(webhook_id, webhook_token): return '', 204 -async def create_message_webhook(guild_id, channel_id, webhook_id, j): - # TODO: impl - pass +async def create_message_webhook(guild_id, channel_id, webhook_id, data): + """Create a message, but for webhooks only.""" + message_id = get_snowflake() + + async with app.db.acquire() as conn: + await pg_set_json(conn) + + await conn.execute( + """ + INSERT INTO messages (id, channel_id, guild_id, webhook_id, + content, tts, mention_everyone, message_type, embeds) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + """, + message_id, + channel_id, + guild_id, + webhook_id, + data['content'], + + data['tts'], + data['everyone_mention'], + + MessageType.DEFAULT.value, + data.get('embeds', []) + ) + + return message_id + @bp.route('/webhooks//', methods=['POST']) @@ -318,6 +345,12 @@ async def execute_webhook(webhook_id: int, webhook_token): # TODO: ensure channel_id points to guild text channel payload_json, files = await msg_create_request() + + # NOTE: we really pop here instead of adding a kwarg + # to msg_create_request just because of webhooks. + # nonce isn't allowed on WEBHOOK_MESSAGE_CREATE + payload_json.pop('nonce') + j = validate(payload_json, WEBHOOK_MESSAGE_CREATE) msg_create_check_content(j, files) @@ -326,13 +359,15 @@ async def execute_webhook(webhook_id: int, webhook_token): mentions_everyone = '@everyone' in j['content'] mentions_here = '@here' in j['content'] + given_embeds = j.get('embeds', []) + message_id = await create_message_webhook( guild_id, channel_id, webhook_id, { 'content': j.get('content', ''), 'tts': j.get('tts', False), 'everyone_mention': mentions_everyone or mentions_here, - 'embeds': [await fill_embed(e) for e in j['embeds']] + 'embeds': await async_map(fill_embed, given_embeds) } ) @@ -341,6 +376,9 @@ async def execute_webhook(webhook_id: int, webhook_token): payload = await app.storage.get_message(message_id) + await app.dispatcher.dispatch('channel', channel_id, + 'MESSAGE_CREATE', payload) + # spawn embedder in the background, even when we're on a webhook. app.sched.spawn( process_url_embed( @@ -349,6 +387,10 @@ async def execute_webhook(webhook_id: int, webhook_token): ) ) + # we can assume its a guild text channel, so just call it + await msg_guild_text_mentions( + payload, guild_id, mentions_everyone, mentions_here) + # TODO: is it really 204? return '', 204 diff --git a/litecord/schemas.py b/litecord/schemas.py index 4d724e2..fd1553e 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -712,11 +712,12 @@ WEBHOOK_MESSAGE_CREATE = { # TODO: url type, or something... 'avatar_url': { - 'type': 'url', 'required': False + # 'type': 'url', 'required': False + 'type': 'string', 'required': False }, 'embeds': { - 'type': list, + 'type': 'list', 'required': False, 'schema': {'type': 'dict', 'schema': EMBED_OBJECT} } diff --git a/litecord/storage.py b/litecord/storage.py index 72812a7..c42977d 100644 --- a/litecord/storage.py +++ b/litecord/storage.py @@ -796,18 +796,28 @@ class Storage: return res async def _inject_author(self, res: dict): - """Inject a pseudo-user object when the message is made by a webhook.""" - author_id, webhook_id = res['author_id'], res['webhook_id'] + """Inject a pseudo-user object when the message is + made by a webhook.""" + author_id = res['author_id'] - if author_id is not None: + # if author_id is None, we fetch webhook info + # from the message_webhook_info table. + if author_id is None: + wb_info = await self.db.fetchrow(""" + SELECT webhook_id, name, avatar + FROM message_webhook_info + WHERE message_id = $1 + """, int(res['id'])) + + res['author'] = { + 'id': str(wb_info['id']), + 'bot': True, + 'username': wb_info['name'], + 'avatar': wb_info['avatar'] + } + else: res['author'] = await self.get_user(res['author_id']) res.pop('webhook_id') - elif webhook_id is not None: - res['author'] = { - 'id': webhook_id, - 'username': 'a', - 'avatar': None - } res.pop('author_id') @@ -815,7 +825,7 @@ class Storage: user_id: Optional[int] = None) -> Optional[Dict]: """Get a single message's payload.""" row = await self.fetchrow_with_json(""" - SELECT id::text, channel_id::text, author_id, webhook_id, content, + SELECT id::text, channel_id::text, author_id, content, created_at AS timestamp, edited_at AS edited_timestamp, tts, mention_everyone, nonce, message_type, embeds FROM messages diff --git a/manage/cmd/migration/16_messages_webhooks.sql b/manage/cmd/migration/16_messages_webhooks.sql new file mode 100644 index 0000000..91aaad2 --- /dev/null +++ b/manage/cmd/migration/16_messages_webhooks.sql @@ -0,0 +1,17 @@ +-- this is a tricky one. blame discord + +-- first, remove all messages made by webhooks +DELETE FROM messages WHERE author_id is null; + +-- delete the row, removing the fkey. no connection anymore. +ALTER TABLE messages DROP COLUMN webhook_id; + +-- add a message_webhook_info table. more on that in Storage._inject_author +CREATE TABLE IF NOT EXISTS message_webhook_info ( + message_id bigint REFERENCES messages (id) PRIMARY KEY, + + webhook_id bigint, + name text DEFAULT '', + avatar text DEFAULT NULL +); + diff --git a/schema.sql b/schema.sql index 6437028..6b93dd7 100644 --- a/schema.sql +++ b/schema.sql @@ -643,13 +643,9 @@ CREATE TABLE IF NOT EXISTS messages ( -- this is good for search. guild_id bigint REFERENCES guilds (id) ON DELETE CASCADE, - -- those are mutually exclusive, only one of them - -- can NOT be NULL at a time. - - -- if author is NULL -> message from webhook - -- if webhook is NULL -> message from author + -- if author is NULL -> message from webhook -> + -- fetch from message_webhook_info author_id bigint REFERENCES users (id), - webhook_id bigint REFERENCES webhooks (id), content text, @@ -666,6 +662,15 @@ CREATE TABLE IF NOT EXISTS messages ( message_type int NOT NULL ); + +CREATE TABLE IF NOT EXISTS message_webhook_info ( + message_id bigint REFERENCES messages (id) PRIMARY KEY, + + webhook_id bigint, + name text DEFAULT '', + avatar text DEFAULT NULL +); + CREATE TABLE IF NOT EXISTS message_reactions ( message_id bigint REFERENCES messages (id), user_id bigint REFERENCES users (id),