channel.messages: add url autoembeds

related to #27

 - embed.sanitizer: handle external config and session objects
 - storage: add Storage.execute_with_json
This commit is contained in:
Luna 2018-12-05 19:07:43 -03:00
parent c934582628
commit 060f225189
3 changed files with 102 additions and 9 deletions

View File

@ -1,5 +1,6 @@
from quart import Blueprint, request, current_app as app, jsonify import re
from quart import Blueprint, request, current_app as app, jsonify
from logbook import Logger from logbook import Logger
@ -12,7 +13,7 @@ from litecord.snowflake import get_snowflake
from litecord.schemas import validate, MESSAGE_CREATE from litecord.schemas import validate, MESSAGE_CREATE
from litecord.utils import pg_set_json from litecord.utils import pg_set_json
from litecord.embed.sanitizer import fill_embed from litecord.embed.sanitizer import fill_embed, proxify, fetch_metadata
log = Logger(__name__) log = Logger(__name__)
@ -226,6 +227,70 @@ async def _guild_text_mentions(payload: dict, guild_id: int,
""", user_id, channel_id) """, user_id, channel_id)
async def process_url_embed(config, storage, dispatcher, session, payload: dict):
message_id = int(payload['id'])
channel_id = int(payload['channel_id'])
# if we already have embeds
# we shouldn't add our own.
embeds = payload['embeds']
if embeds:
log.debug('url processor: ignoring existing embeds @ mid {}',
message_id)
return
# use regex to get URLs
urls = re.findall(r'(https?://\S+)', payload['content'])
urls = urls[:5]
new_embeds = []
# fetch metadata for each url
for url in urls:
img_proxy_url = proxify(url, config=config)
meta = await fetch_metadata(url, config=config, session=session)
if not meta['image']:
continue
new_embeds.append({
'type': 'image',
'url': url,
'thumbnail': {
'width': meta['width'],
'height': meta['height'],
'url': url,
'proxy_url': img_proxy_url
}
})
# update if we got embeds
if not new_embeds:
return
log.debug('made {} thumbnail embeds for mid {}',
len(new_embeds), message_id)
await storage.execute_with_json("""
UPDATE messages
SET embeds = $1
WHERE messages.id = $2
""", new_embeds, message_id)
update_payload = {
'id': str(message_id),
'channel_id': str(channel_id),
'embeds': new_embeds,
}
if 'guild_id' in payload:
update_payload['guild_id'] = payload['guild_id']
await dispatcher.dispatch(
'channel', channel_id, 'MESSAGE_UPDATE', update_payload)
@bp.route('/<int:channel_id>/messages', methods=['POST']) @bp.route('/<int:channel_id>/messages', methods=['POST'])
async def _create_message(channel_id): async def _create_message(channel_id):
"""Create a message.""" """Create a message."""
@ -276,6 +341,10 @@ async def _create_message(channel_id):
await app.dispatcher.dispatch('channel', channel_id, await app.dispatcher.dispatch('channel', channel_id,
'MESSAGE_CREATE', payload) 'MESSAGE_CREATE', payload)
app.sched.spawn(
process_url_embed(app.config, app.storage, app.dispatcher, app.session,
payload))
# update read state for the author # update read state for the author
await app.db.execute(""" await app.db.execute("""
UPDATE user_read_state UPDATE user_read_state

View File

@ -8,6 +8,8 @@ from typing import Dict, Any
from logbook import Logger from logbook import Logger
from quart import current_app as app from quart import current_app as app
from litecord.embed.schemas import EmbedURL
log = Logger(__name__) log = Logger(__name__)
Embed = Dict[str, Any] Embed = Dict[str, Any]
@ -55,12 +57,18 @@ def path_exists(embed: Embed, components: str):
return False return False
def proxify(url) -> str: def proxify(url, *, config=None) -> str:
"""Return a mediaproxy url for the given EmbedURL.""" """Return a mediaproxy url for the given EmbedURL."""
md_base_url = app.config['MEDIA_PROXY'] if not config:
config = app.config
if isinstance(url, str):
url = EmbedURL(url)
md_base_url = config['MEDIA_PROXY']
parsed = url.parsed parsed = url.parsed
proto = 'https' if app.config['IS_SSL'] else 'http' proto = 'https' if config['IS_SSL'] else 'http'
return ( return (
# base mediaproxy url # base mediaproxy url
@ -69,18 +77,28 @@ def proxify(url) -> str:
) )
async def fetch_metadata(url) -> dict: async def fetch_metadata(url, *, config=None, session=None) -> dict:
"""Fetch metadata for a url.""" """Fetch metadata for a url."""
if session is None:
session = app.session
if config is None:
config = app.config
if isinstance(url, str):
url = EmbedURL(url)
parsed = url.parsed parsed = url.parsed
md_path = f'{parsed.scheme}/{parsed.netloc}{parsed.path}' md_path = f'{parsed.scheme}/{parsed.netloc}{parsed.path}'
md_base_url = app.config['MEDIA_PROXY'] md_base_url = config['MEDIA_PROXY']
proto = 'https' if app.config['IS_SSL'] else 'http' proto = 'https' if config['IS_SSL'] else 'http'
request_url = f'{proto}://{md_base_url}/meta/{md_path}' request_url = f'{proto}://{md_base_url}/meta/{md_path}'
async with app.session.get(request_url) as resp: async with session.get(request_url) as resp:
if resp.status != 200: if resp.status != 200:
return return

View File

@ -64,6 +64,12 @@ class Storage:
await pg_set_json(con) await pg_set_json(con)
return await con.fetch(query, *args) return await con.fetch(query, *args)
async def execute_with_json(self, query: str, *args):
"""Execute a SQL statement with JSON/JSONB support."""
async with self.db.acquire() as con:
await pg_set_json(con)
return await con.execute(query, *args)
async def get_user(self, user_id, secure=False) -> Dict[str, Any]: async def get_user(self, user_id, secure=False) -> Dict[str, Any]:
"""Get a single user payload.""" """Get a single user payload."""
user_id = int(user_id) user_id = int(user_id)