2023-05-24 22:18:31 +00:00
|
|
|
"""Serves /convert/... URLs to convert data from one protocol to another.
|
|
|
|
|
|
|
|
URL pattern is /convert/SOURCE/DEST , where SOURCE and DEST are the LABEL
|
|
|
|
constants from the :class:`Protocol` subclasses.
|
|
|
|
|
2023-05-30 19:15:36 +00:00
|
|
|
Currently only supports /convert/activitypub/web/...
|
2023-05-24 22:18:31 +00:00
|
|
|
"""
|
|
|
|
import logging
|
|
|
|
import re
|
|
|
|
import urllib.parse
|
|
|
|
|
2023-05-24 23:00:41 +00:00
|
|
|
from flask import redirect, request
|
|
|
|
from granary import as1
|
2023-05-24 22:18:31 +00:00
|
|
|
from oauth_dropins.webutil import flask_util, util
|
|
|
|
from oauth_dropins.webutil.flask_util import error
|
|
|
|
|
|
|
|
from activitypub import ActivityPub
|
|
|
|
from common import CACHE_TIME
|
|
|
|
from flask_app import app, cache
|
2023-05-26 23:07:36 +00:00
|
|
|
from models import Object, PROTOCOLS
|
2023-05-27 00:40:29 +00:00
|
|
|
from web import Web
|
2023-05-24 22:18:31 +00:00
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
SOURCES = frozenset((
|
|
|
|
ActivityPub.LABEL,
|
|
|
|
))
|
|
|
|
DESTS = frozenset((
|
2023-05-27 00:40:29 +00:00
|
|
|
Web.LABEL,
|
2023-05-24 22:18:31 +00:00
|
|
|
))
|
|
|
|
|
|
|
|
|
2023-05-24 23:00:41 +00:00
|
|
|
@app.get(f'/convert/<any({",".join(SOURCES)}):src>/<any({",".join(DESTS)}):dest>/<path:_>')
|
2023-05-24 22:18:31 +00:00
|
|
|
@flask_util.cached(cache, CACHE_TIME, headers=['Accept'])
|
2023-05-24 23:00:41 +00:00
|
|
|
def convert(src, dest, _):
|
2023-05-24 22:18:31 +00:00
|
|
|
"""Converts data from one protocol to another and serves it.
|
|
|
|
|
|
|
|
Fetches the source data if it's not already stored.
|
|
|
|
"""
|
2023-05-24 23:00:41 +00:00
|
|
|
# don't use urllib.parse.urlencode(request.args) because that doesn't
|
|
|
|
# guarantee us the same query param string as in the original URL, and we
|
|
|
|
# want exactly the same thing since we're looking up the URL's Object by id
|
|
|
|
path_prefix = f'convert/{src}/{dest}/'
|
|
|
|
url = request.url.removeprefix(request.root_url).removeprefix(path_prefix)
|
|
|
|
|
2023-05-24 23:31:42 +00:00
|
|
|
# our redirects evidently collapse :// down to :/ , maybe to prevent URL
|
|
|
|
# parsing bugs? if that happened to this URL, expand it back to ://
|
2023-05-24 22:18:31 +00:00
|
|
|
url = re.sub(r'^(https?:/)([^/])', r'\1/\2', url)
|
|
|
|
|
|
|
|
if not util.is_web(url):
|
|
|
|
error(f'Expected fully qualified URL; got {url}')
|
|
|
|
|
2023-05-24 23:00:41 +00:00
|
|
|
# load, and maybe fetch. if it's a post/update, redirect to inner object.
|
2023-05-26 23:07:36 +00:00
|
|
|
obj = PROTOCOLS[src].load(url)
|
2023-05-24 23:00:41 +00:00
|
|
|
if not obj.as1:
|
|
|
|
error(f'Stored object for {id} has no data', status=404)
|
|
|
|
|
|
|
|
type = as1.object_type(obj.as1)
|
|
|
|
if type in ('post', 'update', 'delete'):
|
|
|
|
obj_id = as1.get_object(obj.as1).get('id')
|
|
|
|
if obj_id:
|
2023-05-26 23:07:36 +00:00
|
|
|
# TODO: PROTOCOLS[src].load() this instead?
|
2023-05-24 23:00:41 +00:00
|
|
|
obj_obj = Object.get_by_id(obj_id)
|
|
|
|
if (obj_obj and obj_obj.as1 and
|
|
|
|
not obj_obj.as1.keys() <= set(['id', 'url', 'objectType'])):
|
|
|
|
logger.info(f'{type} activity, redirecting to Object {obj_id}')
|
|
|
|
return redirect(f'/{path_prefix}{obj_id}', code=301)
|
|
|
|
|
|
|
|
# don't serve deletes or deleted objects
|
|
|
|
if obj.deleted or type == 'delete':
|
|
|
|
return '', 410
|
|
|
|
|
|
|
|
# convert and serve
|
2023-05-26 23:07:36 +00:00
|
|
|
return PROTOCOLS[dest].serve(obj)
|
2023-05-24 23:09:44 +00:00
|
|
|
|
|
|
|
|
|
|
|
@app.get('/render')
|
|
|
|
def render_redirect():
|
|
|
|
"""Redirect from old /render?id=... endpoint to /convert/..."""
|
|
|
|
id = flask_util.get_required_param('id')
|
2023-05-30 19:15:36 +00:00
|
|
|
return redirect(f'/convert/activitypub/web/{id}', code=301)
|