From 404783534c23a8ddb14e92f58791297cd94efbf4 Mon Sep 17 00:00:00 2001 From: Luna Date: Tue, 19 Mar 2019 00:04:42 -0300 Subject: [PATCH] webhooks: add draft for avatar_url on webhook execute this is a bit messy due to us having to call mediaproxy for the given url, then store it on icons manager. - embed.messages: add is_media_url - embed.sanitizer: add fetch_raw_img - embed.schemas: add EmbedURL.to_md_path --- litecord/blueprints/icons.py | 1 + litecord/blueprints/webhooks.py | 49 +++++++++++++++++++++++++++++--- litecord/embed/messages.py | 14 +++++---- litecord/embed/sanitizer.py | 50 +++++++++++++++++++++++++-------- litecord/embed/schemas.py | 6 ++++ 5 files changed, 100 insertions(+), 20 deletions(-) diff --git a/litecord/blueprints/icons.py b/litecord/blueprints/icons.py index 7434bad..1525f2b 100644 --- a/litecord/blueprints/icons.py +++ b/litecord/blueprints/icons.py @@ -27,6 +27,7 @@ async def send_icon(scope, key, icon_hash, **kwargs): """Send an icon.""" icon = await app.icons.generic_get( scope, key, icon_hash, **kwargs) + if not icon: return '', 404 diff --git a/litecord/blueprints/webhooks.py b/litecord/blueprints/webhooks.py index 30b9c23..d17c87e 100644 --- a/litecord/blueprints/webhooks.py +++ b/litecord/blueprints/webhooks.py @@ -18,6 +18,7 @@ along with this program. If not, see . """ import secrets +import base64 from typing import Dict, Any, Optional from quart import Blueprint, jsonify, current_app as app, request @@ -33,14 +34,16 @@ from litecord.schemas import ( from litecord.enums import ChannelType from litecord.snowflake import get_snowflake from litecord.utils import async_map -from litecord.errors import WebhookNotFound, Unauthorized, ChannelNotFound +from litecord.errors import ( + WebhookNotFound, Unauthorized, ChannelNotFound, BadRequest +) from litecord.blueprints.channel.messages import ( msg_create_request, msg_create_check_content, msg_add_attachment, msg_guild_text_mentions ) -from litecord.embed.sanitizer import fill_embed -from litecord.embed.messages import process_url_embed +from litecord.embed.sanitizer import fill_embed, fetch_raw_img +from litecord.embed.messages import process_url_embed, is_media_url from litecord.utils import pg_set_json from litecord.enums import MessageType @@ -335,6 +338,32 @@ async def create_message_webhook(guild_id, channel_id, webhook_id, data): return message_id +async def _create_avatar(webhook_id: int, avatar_url): + """Create an avatar for a webhook out of an avatar URL, + given when executing the webhook. + + Litecord will query that URL via mediaproxy and store the data + via IconManager. + """ + if avatar_url.scheme not in ('http', 'https'): + raise BadRequest('invalid avatar url scheme') + + if not is_media_url(avatar_url): + raise BadRequest('url is not media url') + + resp, raw = await fetch_raw_img(avatar_url) + raw_b64 = base64.b64encode(raw) + + mime = resp.headers['content-type'] + b64_data = f'data:{mime};base64,{raw_b64}' + + icon = await app.icons.put( + 'user', webhook_id, b64_data, + always_icon=True, size=(128, 128) + ) + + return icon.icon_hash + @bp.route('/webhooks//', methods=['POST']) async def execute_webhook(webhook_id: int, webhook_token): @@ -361,13 +390,25 @@ async def execute_webhook(webhook_id: int, webhook_token): given_embeds = j.get('embeds', []) + webhook = await get_webhook(webhook_id) + avatar = webhook['avatar'] + + if 'avatar_url' in j and j['avatar_url'] is not None: + avatar = await _create_avatar(webhook_id, j['avatar_url']) + 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 async_map(fill_embed, given_embeds) + 'embeds': await async_map(fill_embed, given_embeds), + + 'info': { + 'id': webhook_id, + 'name': j.get('name', webhook['name']), + 'avatar': avatar + } } ) diff --git a/litecord/embed/messages.py b/litecord/embed/messages.py index f13b60e..70bf6f9 100644 --- a/litecord/embed/messages.py +++ b/litecord/embed/messages.py @@ -85,6 +85,14 @@ async def _update_and_dispatch(payload, new_embeds, storage, dispatcher): 'channel', channel_id, 'MESSAGE_UPDATE', update_payload) +def is_media_url(url: str) -> bool: + parsed = urllib.parse.urlparse(url) + path = Path(parsed.path) + extension = path.suffix.lstrip('.') + + return extension in MEDIA_EXTENSIONS: + + async def insert_mp_embed(parsed, config, session): """Insert mediaproxy embed.""" embed = await fetch_embed(parsed, config=config, session=session) @@ -125,11 +133,7 @@ async def process_url_embed(config, storage, dispatcher, new_embeds = [] for url in urls: - parsed = urllib.parse.urlparse(url) - path = Path(parsed.path) - extension = path.suffix.lstrip('.') - - if extension in MEDIA_EXTENSIONS: + if is_media_url(url): embed = await insert_media_meta(url, config, session) else: embed = await insert_mp_embed(parsed, config, session) diff --git a/litecord/embed/sanitizer.py b/litecord/embed/sanitizer.py index ef912c4..40959c0 100644 --- a/litecord/embed/sanitizer.py +++ b/litecord/embed/sanitizer.py @@ -96,26 +96,32 @@ def proxify(url, *, config=None) -> str: ) -async def fetch_metadata(url, *, config=None, session=None) -> Optional[Dict]: - """Fetch metadata for a url.""" +def _mk_cfg_sess(config, session) -> tuple: + if config is None: + config = app.config if session is None: session = app.session - if config is None: - config = app.config + return config, session - if isinstance(url, str): - url = EmbedURL(url) - - parsed = url.parsed - - md_path = f'{parsed.scheme}/{parsed.netloc}{parsed.path}' +def _md_base(config) -> tuple: md_base_url = config['MEDIA_PROXY'] proto = 'https' if config['IS_SSL'] else 'http' - request_url = f'{proto}://{md_base_url}/meta/{md_path}' + return proto, md_base_url + + +async def fetch_metadata(url, *, config=None, session=None) -> Optional[Dict]: + """Fetch metadata for a url.""" + config, session = _mk_cfg_sess(config, session) + + if not isinstance(url, EmbedURL): + url = EmbedURL(url) + + proto, md_base_url = _md_base(config) + request_url = f'{proto}://{md_base_url}/meta/{url.to_md_path}' async with session.get(request_url) as resp: if resp.status != 200: @@ -128,6 +134,28 @@ async def fetch_metadata(url, *, config=None, session=None) -> Optional[Dict]: return await resp.json() +async def fetch_raw_img(url, *, config=None, session=None) -> Optional[tuple]: + """Fetch metadata for a url.""" + config, session = _mk_cfg_sess(config, session) + + if not isinstance(url, EmbedURL): + url = EmbedURL(url) + + proto, md_base_url = _md_base(config) + # NOTE: the img, instead of /meta/. + request_url = f'{proto}://{md_base_url}/img/{url.to_md_path}' + + async with session.get(request_url) as resp: + if resp.status != 200: + body = await resp.text() + + log.warning('failed to get img for {!r}: {} {!r}', + url, resp.status, body) + return None + + return resp, await resp.read() + + async def fetch_embed(parsed, *, config=None, session=None) -> dict: """Fetch an embed""" diff --git a/litecord/embed/schemas.py b/litecord/embed/schemas.py index 63d8246..819e072 100644 --- a/litecord/embed/schemas.py +++ b/litecord/embed/schemas.py @@ -45,6 +45,12 @@ class EmbedURL: """'json' version of the url.""" return self.url + @property + def to_md_path(self) -> str: + """Convert the EmbedURL to a mediaproxy path.""" + parsed = self.parsed + return f'{parsed.scheme}/{parsed.netloc}{parsed.path}' + EMBED_FOOTER = { 'text': {