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
This commit is contained in:
Luna 2019-03-19 00:04:42 -03:00
parent 1d9c9f7b85
commit 404783534c
5 changed files with 100 additions and 20 deletions

View File

@ -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

View File

@ -18,6 +18,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
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/<int:webhook_id>/<webhook_token>', 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
}
}
)

View File

@ -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)

View File

@ -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"""

View File

@ -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': {