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