diff --git a/litecord/blueprints/icons.py b/litecord/blueprints/icons.py
index 1525f2b..b01bac7 100644
--- a/litecord/blueprints/icons.py
+++ b/litecord/blueprints/icons.py
@@ -18,7 +18,11 @@ along with this program. If not, see .
"""
from os.path import splitext
-from quart import Blueprint, current_app as app, send_file
+
+from quart import Blueprint, current_app as app, send_file, redirect
+
+from litecord.embed.sanitizer import make_md_req_url
+from litecord.embed.schemas import EmbedURL
bp = Blueprint('images', __name__)
@@ -54,14 +58,32 @@ async def _get_guild_icon(guild_id: int, icon_file: str):
return await send_icon('guild', guild_id, icon_hash, ext=ext)
-@bp.route('/embed/avatars/.png')
-async def _get_default_user_avatar(discrim: int):
+@bp.route('/embed/avatars/.png')
+async def _get_default_user_avatar(default_id: int):
+ # TODO: how do we determine which assets to use for this?
+ # I don't think we can use discord assets.
pass
+async def _handle_webhook_avatar(md_url_redir: str):
+ md_url = make_md_req_url(app.config, 'img', EmbedURL(md_url_redir))
+ return redirect(md_url)
+
+
@bp.route('/avatars//')
async def _get_user_avatar(user_id, avatar_file):
avatar_hash, ext = splitext_(avatar_file)
+
+ # first, check if this is a webhook avatar to redir to
+ md_url_redir = await app.db.fetchval("""
+ SELECT md_url_redir
+ FROM webhook_avatars
+ WHERE webhook_id = $1 AND hash = $2
+ """, user_id, avatar_hash)
+
+ if md_url_redir:
+ return await _handle_webhook_avatar(md_url_redir)
+
return await send_icon('user', user_id, avatar_hash, ext=ext)
diff --git a/litecord/blueprints/webhooks.py b/litecord/blueprints/webhooks.py
index fb91af0..08712d3 100644
--- a/litecord/blueprints/webhooks.py
+++ b/litecord/blueprints/webhooks.py
@@ -18,9 +18,10 @@ along with this program. If not, see .
"""
import secrets
-import base64
+import hashlib
from typing import Dict, Any, Optional
+import asyncpg
from quart import Blueprint, jsonify, current_app as app, request
from litecord.auth import token_check
@@ -44,8 +45,10 @@ from litecord.blueprints.channel.messages import (
)
from litecord.embed.sanitizer import fill_embed, fetch_raw_img
from litecord.embed.messages import process_url_embed, is_media_url
+from litecord.embed.schemas import EmbedURL
from litecord.utils import pg_set_json
from litecord.enums import MessageType
+from litecord.images import STATIC_IMAGE_MIMES
bp = Blueprint('webhooks', __name__)
@@ -346,12 +349,27 @@ async def create_message_webhook(guild_id, channel_id, webhook_id, data):
return message_id
-async def _create_avatar(webhook_id: int, avatar_url):
+async def _webhook_avy_redir(webhook_id: int, avatar_url: EmbedURL):
+ """Create a row on webhook_avatars."""
+ url_hash = hashlib.sha256(avatar_url.to_md_path.encode()).hexdigest()
+
+ try:
+ await app.db.execute("""
+ INSERT INTO webhook_avatars (webhook_id, hash, md_url_redir)
+ VALUES ($1, $2, $3)
+ """, webhook_id, url_hash, avatar_url.url)
+ except asyncpg.UniqueViolationError:
+ pass
+
+ return url_hash
+
+
+async def _create_avatar(webhook_id: int, avatar_url: EmbedURL) -> str:
"""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.
+ Litecord will write an URL that redirects to the given avatar_url,
+ using mediaproxy.
"""
if avatar_url.scheme not in ('http', 'https'):
raise BadRequest('invalid avatar url scheme')
@@ -359,18 +377,27 @@ async def _create_avatar(webhook_id: int, avatar_url):
if not is_media_url(avatar_url):
raise BadRequest('url is not media url')
+ # we still fetch the URL to check its validity, mimetypes, etc
+ # but in the end, we will store it under the webhook_avatars table,
+ # not IconManager.
resp, raw = await fetch_raw_img(avatar_url)
- raw_b64 = base64.b64encode(raw).decode()
+ #raw_b64 = base64.b64encode(raw).decode()
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)
- )
+ # TODO: apng checks are missing (for this and everywhere else)
+ if mime not in STATIC_IMAGE_MIMES:
+ raise BadRequest('invalid mime type for given url')
- return icon.icon_hash
+ #b64_data = f'data:{mime};base64,{raw_b64}'
+
+ # TODO: replace this by webhook_avatars
+ #icon = await app.icons.put(
+ # 'user', webhook_id, b64_data,
+ # always_icon=True, size=(128, 128)
+ #)
+
+ return await _webhook_avy_redir(webhook_id, avatar_url)
@bp.route('/webhooks//', methods=['POST'])
@@ -399,6 +426,10 @@ async def execute_webhook(webhook_id: int, webhook_token):
given_embeds = j.get('embeds', [])
webhook = await get_webhook(webhook_id)
+
+ # webhooks have TWO avatars. one is from settings, the other is from
+ # the json's icon_url. one can be handled gracefully by IconManager,
+ # but the other can't, at all.
avatar = webhook['avatar']
if 'avatar_url' in j and j['avatar_url'] is not None:
diff --git a/litecord/embed/sanitizer.py b/litecord/embed/sanitizer.py
index d545a79..1f7968f 100644
--- a/litecord/embed/sanitizer.py
+++ b/litecord/embed/sanitizer.py
@@ -97,7 +97,7 @@ def _md_base(config) -> tuple:
return proto, md_base_url
-def _make_md_req_url(config, scope: str, url):
+def make_md_req_url(config, scope: str, url):
"""Make a mediaproxy request URL given the config, scope, and the url
to be proxied."""
proto, base_url = _md_base(config)
@@ -112,7 +112,7 @@ def proxify(url, *, config=None) -> str:
if isinstance(url, str):
url = EmbedURL(url)
- return _make_md_req_url(config, 'img', url)
+ return make_md_req_url(config, 'img', url)
async def _md_client_req(config, session, scope: str,
@@ -152,7 +152,7 @@ async def _md_client_req(config, session, scope: str,
if not isinstance(url, EmbedURL):
url = EmbedURL(url)
- request_url = _make_md_req_url(config, scope, url)
+ request_url = make_md_req_url(config, scope, url)
async with session.get(request_url) as resp:
if resp.status == 200:
diff --git a/litecord/embed/schemas.py b/litecord/embed/schemas.py
index ef60702..6f3b306 100644
--- a/litecord/embed/schemas.py
+++ b/litecord/embed/schemas.py
@@ -35,6 +35,11 @@ class EmbedURL:
self.raw_url = url
self.parsed = parsed
+ @classmethod
+ def from_parsed(cls, parsed):
+ """Make an EmbedURL instance out of an already parsed 6-tuple."""
+ return cls(parsed.geturl())
+
@property
def url(self) -> str:
"""Return the unparsed URL."""
diff --git a/litecord/images.py b/litecord/images.py
index ec37469..0ce0595 100644
--- a/litecord/images.py
+++ b/litecord/images.py
@@ -50,6 +50,11 @@ MIMES = {
'webp': 'image/webp',
}
+STATIC_IMAGE_MIMES = [
+ 'image/png',
+ 'image/jpeg',
+ 'image/webp'
+]
def get_ext(mime: str) -> str:
if mime in EXTENSIONS:
diff --git a/manage/cmd/migration/command.py b/manage/cmd/migration/command.py
index e769b6c..1f1f91f 100644
--- a/manage/cmd/migration/command.py
+++ b/manage/cmd/migration/command.py
@@ -228,7 +228,7 @@ async def migrate_cmd(app, _args):
migration = ctx.scripts.get(idx)
print('applying', migration.id, migration.name)
- # await apply_migration(app, migration)
+ await apply_migration(app, migration)
def setup(subparser):
diff --git a/manage/cmd/migration/scripts/1_webhook_avatars.sql b/manage/cmd/migration/scripts/1_webhook_avatars.sql
new file mode 100644
index 0000000..150efbb
--- /dev/null
+++ b/manage/cmd/migration/scripts/1_webhook_avatars.sql
@@ -0,0 +1,13 @@
+-- webhook_avatars table. check issue 46.
+CREATE TABLE IF NOT EXISTS webhook_avatars (
+ webhook_id bigint,
+
+ -- this is e.g a sha256 hash of EmbedURL.to_md_url
+ hash text,
+
+ -- we don't hardcode the mediaproxy url here for obvious reasons.
+ -- the output of EmbedURL.to_md_url goes here.
+ md_url_redir text,
+
+ PRIMARY KEY (webhook_id, hash)
+);