From 712f1d9ff20922b4a6606ff79e2bd8cd97012979 Mon Sep 17 00:00:00 2001 From: Luna Date: Mon, 18 Mar 2019 04:35:44 -0300 Subject: [PATCH] webhooks: add drafts for execution impl - channel.messages: export msg_create_request, msg_create_check_payload, msg_add_attachment - schemas: add WEBHOOK_MESSAGE_CREATE --- litecord/blueprints/channel/messages.py | 24 +++++--- litecord/blueprints/webhooks.py | 74 ++++++++++++++++++++++--- litecord/schemas.py | 24 ++++++++ 3 files changed, 106 insertions(+), 16 deletions(-) diff --git a/litecord/blueprints/channel/messages.py b/litecord/blueprints/channel/messages.py index 41850e2..2fdd904 100644 --- a/litecord/blueprints/channel/messages.py +++ b/litecord/blueprints/channel/messages.py @@ -249,7 +249,7 @@ async def _guild_text_mentions(payload: dict, guild_id: int, """, user_id, channel_id) -async def _msg_input() -> tuple: +async def msg_create_request() -> tuple: """Extract the json input and any file information the client gave to us in the request. @@ -277,26 +277,34 @@ async def _msg_input() -> tuple: return json_from_form, [v for k, v in files.items()] -def _check_content(payload: dict, files: list): +def msg_create_check_content(payload: dict, files: list, *, use_embeds=False): """Check if there is actually any content being sent to us.""" has_content = bool(payload.get('content', '')) - has_embed = 'embed' in payload has_files = len(files) > 0 + embed_field = 'embeds' if use_embeds else 'embed' + has_embed = embed_field in payload + has_total_content = has_content or has_embed or has_files if not has_total_content: raise BadRequest('No content has been provided.') -async def _add_attachment(message_id: int, channel_id: int, - attachment_file) -> int: +async def msg_add_attachment(message_id: int, channel_id: int, + attachment_file) -> int: """Add an attachment to a message. Parameters ---------- message_id: int The ID of the message getting the attachment. + channel_id: int + The ID of the channel the message belongs to. + + Exists because the attachment URL scheme contains + a channel id. The purpose is unknown, but we are + implementing Discord's behavior. attachment_file: quart.FileStorage quart FileStorage instance of the file. """ @@ -362,10 +370,10 @@ async def _create_message(channel_id): await channel_perm_check(user_id, channel_id, 'send_messages') actual_guild_id = guild_id - payload_json, files = await _msg_input() + payload_json, files = await msg_create_request() j = validate(payload_json, MESSAGE_CREATE) - _check_content(payload_json, files) + msg_create_check_content(payload_json, files) # TODO: check connection to the gateway @@ -399,7 +407,7 @@ async def _create_message(channel_id): # for each file given, we add it as an attachment for pre_attachment in files: - await _add_attachment(message_id, channel_id, pre_attachment) + await msg_add_attachment(message_id, channel_id, pre_attachment) payload = await app.storage.get_message(message_id, user_id) diff --git a/litecord/blueprints/webhooks.py b/litecord/blueprints/webhooks.py index 3e7f99d..36a686f 100644 --- a/litecord/blueprints/webhooks.py +++ b/litecord/blueprints/webhooks.py @@ -27,12 +27,21 @@ from litecord.blueprints.checks import ( channel_check, channel_perm_check, guild_check, guild_perm_check ) -from litecord.schemas import validate, WEBHOOK_CREATE, WEBHOOK_UPDATE +from litecord.schemas import ( + validate, WEBHOOK_CREATE, WEBHOOK_UPDATE, WEBHOOK_MESSAGE_CREATE +) from litecord.enums import ChannelType from litecord.snowflake import get_snowflake from litecord.utils import async_map from litecord.errors import WebhookNotFound, Unauthorized +from litecord.blueprints.channel.messages import ( + msg_create_request, msg_create_check_content, msg_add_attachment, + # create_message +) +from litecord.embed.sanitizer import fill_embed +from litecord.embed.messages import process_url_embed + bp = Blueprint('webhooks', __name__) @@ -108,15 +117,17 @@ async def _webhook_many(where_clause, arg: int): async def webhook_token_check(webhook_id: int, webhook_token: str): """token_check() equivalent for webhooks.""" - webhook_id_fetch = await app.db.fetchrow(""" - SELECT id + row = await app.db.fetchrow(""" + SELECT guild_id, channel_id FROM webhooks WHERE id = $1 AND token = $2 """, webhook_id, webhook_token) - if webhook_id_fetch is None: + if row is None: raise Unauthorized('webhook not found or unauthorized') + return row['guild_id'], row['channel_id'] + @bp.route('/channels//webhooks', methods=['POST']) async def create_webhook(channel_id: int): @@ -259,17 +270,64 @@ async def del_webhook_tokened(webhook_id, webhook_token): return '', 204 -@bp.route('/webhooks//', methods=['POST']) -async def execute_webhook(webhook_id, webhook_token): +async def create_message_webhook(guild_id, channel_id, webhook_id, j): + # TODO: impl pass +@bp.route('/webhooks//', methods=['POST']) +async def execute_webhook(webhook_id: int, webhook_token): + """Execute a webhook. Sends a message to the channel the webhook + is tied to.""" + guild_id, channel_id = await webhook_token_check(webhook_id, webhook_token) + + # TODO: ensure channel_id points to guild text channel + + payload_json, files = await msg_create_request() + j = validate(payload_json, WEBHOOK_MESSAGE_CREATE) + + msg_create_check_content(j, files) + + # webhooks don't need permissions. + mentions_everyone = '@everyone' in j['content'] + mentions_here = '@here' in j['content'] + + 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']] + } + ) + + for pre_attachment in files: + await msg_add_attachment(message_id, channel_id, pre_attachment) + + payload = await app.storage.get_message(message_id) + + # spawn embedder in the background, even when we're on a webhook. + app.sched.spawn( + process_url_embed( + app.config, app.storage, app.dispatcher, app.session, + payload + ) + ) + + # TODO: is it really 204? + return '', 204 + @bp.route('/webhooks///slack', methods=['POST']) async def execute_slack_webhook(webhook_id, webhook_token): - pass + """Execute a webhook but expecting Slack data.""" + # TODO: know slack webhooks + await webhook_token_check(webhook_id, webhook_token) @bp.route('/webhooks///github', methods=['POST']) async def execute_github_webhook(webhook_id, webhook_token): - pass + """Execute a webhook but expecting GitHub data.""" + # TODO: know github webhooks + await webhook_token_check(webhook_id, webhook_token) diff --git a/litecord/schemas.py b/litecord/schemas.py index 10dedf1..4d724e2 100644 --- a/litecord/schemas.py +++ b/litecord/schemas.py @@ -697,3 +697,27 @@ WEBHOOK_UPDATE = { 'avatar': {'type': 'b64_icon', 'required': False, 'nullable': False}, 'channel_id': {'coerce': int, 'required': False, 'nullable': False} } + +WEBHOOK_MESSAGE_CREATE = { + 'content': { + 'type': 'string', + 'minlength': 0, 'maxlength': 2000, 'required': False + }, + 'tts': {'type': 'boolean', 'required': False}, + + 'username': { + 'type': 'string', + 'minlength': 2, 'maxlength': 32, 'required': False + }, + + # TODO: url type, or something... + 'avatar_url': { + 'type': 'url', 'required': False + }, + + 'embeds': { + 'type': list, + 'required': False, + 'schema': {'type': 'dict', 'schema': EMBED_OBJECT} + } +}