diff --git a/app.py b/app.py
index 3cf62cf..e09d964 100644
--- a/app.py
+++ b/app.py
@@ -6,4 +6,4 @@ registered.
from flask_app import app
# import all modules to register their Flask handlers
-import activitypub, convert, follow, pages, redirect, render, superfeedr, webfinger, webmention, xrpc_actor, xrpc_feed, xrpc_graph
+import activitypub, convert, follow, pages, redirect, superfeedr, webfinger, webmention, xrpc_actor, xrpc_feed, xrpc_graph
diff --git a/convert.py b/convert.py
index a670268..4e221ff 100644
--- a/convert.py
+++ b/convert.py
@@ -73,3 +73,10 @@ def convert(src, dest, _):
# convert and serve
return protocols[dest].serve(obj)
+
+
+@app.get('/render')
+def render_redirect():
+ """Redirect from old /render?id=... endpoint to /convert/..."""
+ id = flask_util.get_required_param('id')
+ return redirect(f'/convert/activitypub/webmention/{id}', code=301)
diff --git a/render.py b/render.py
deleted file mode 100644
index 4abec59..0000000
--- a/render.py
+++ /dev/null
@@ -1,46 +0,0 @@
-"""Renders mf2 proxy pages based on stored Object entities."""
-import datetime
-import logging
-from urllib.parse import urlencode
-
-from flask import redirect, request
-from granary import as1, as2, atom, microformats2
-from oauth_dropins.webutil import flask_util
-from oauth_dropins.webutil.flask_util import error
-from oauth_dropins.webutil import util
-
-import activitypub
-from flask_app import app, cache
-import common
-from models import Object
-from webmention import Webmention
-
-logger = logging.getLogger(__name__)
-
-
-@app.get('/render')
-@flask_util.cached(cache, common.CACHE_TIME)
-def render():
- """Fetches a stored Object and renders it as HTML."""
- id = flask_util.get_required_param('id')
- obj = Object.get_by_id(id)
- if not obj:
- error(f'No stored object for {id}', status=404)
- elif not obj.as1:
- error(f'Stored object for {id} has no AS1', status=404)
-
- # redirect creates, updates, etc to inner object
- type = as1.object_type(obj.as1)
- if type in ('post', 'update', 'delete'):
- obj_id = as1.get_object(obj.as1).get('id')
- if obj_id:
- 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('/render?' + urlencode({'id': obj_id}), code=301)
-
- if obj.deleted or type == 'delete':
- return '', 410
-
- return Webmention.serve(obj)
diff --git a/tests/test_convert.py b/tests/test_convert.py
index a4b3b97..2ec64e1 100644
--- a/tests/test_convert.py
+++ b/tests/test_convert.py
@@ -206,3 +206,9 @@ A ☕ reply
""", resp.get_data(as_text=True), ignore_blanks=True)
+
+ def test_render_endpoint_redirect(self):
+ resp = self.client.get('/render?id=http://foo%3Fbar')
+ self.assertEqual(301, resp.status_code)
+ self.assertEqual(f'/convert/activitypub/webmention/http://foo?bar',
+ resp.headers['Location'])
diff --git a/tests/test_render.py b/tests/test_render.py
deleted file mode 100644
index 809c3c0..0000000
--- a/tests/test_render.py
+++ /dev/null
@@ -1,165 +0,0 @@
-"""Unit tests for render.py."""
-import copy
-
-from granary import as2
-from granary.tests.test_as1 import ACTOR, COMMENT, DELETE_OF_ID, UPDATE
-
-from flask_app import app
-import common
-from models import Object
-import render
-from . import testutil
-
-EXPECTED_HTML = """\
-
-
-
-
-
-
- tag:fake.com:123456
-
- fake.com/123456
-
- A ☕ reply
-
-
-
-
-
-"""
-EXPECTED_AUTHOR_HTML = """\
-
-
-
-
-
-
- tag:fake.com:123456
-
-
-
- Bob
-
-
- fake.com/123456
-
- A ☕ reply
-
-
-
-
-
-"""
-
-class RenderTest(testutil.TestCase):
-
- def test_render_errors(self):
- resp = self.client.get(f'/render?id=')
- self.assertEqual(400, resp.status_code)
-
- resp = self.client.get(f'/render')
- self.assertEqual(400, resp.status_code)
-
- # no Object
- resp = self.client.get('/render?id=abc')
- self.assertEqual(404, resp.status_code)
-
- def test_render(self):
- with self.request_context:
- Object(id='abc', as2=as2.from_as1(COMMENT)).put()
- resp = self.client.get('/render?id=abc')
- self.assertEqual(200, resp.status_code)
- self.assert_multiline_equals(EXPECTED_HTML, resp.get_data(as_text=True), ignore_blanks=True)
-
- def test_render_with_author(self):
- with self.request_context:
- Object(id='abc', as2=as2.from_as1({**COMMENT, 'author': 'def'}),
- source_protocol='activitypub').put()
- Object(id='def', as2=as2.from_as1(ACTOR),
- source_protocol='activitypub').put()
-
- resp = self.client.get('/render?id=abc')
- self.assertEqual(200, resp.status_code)
- self.assert_multiline_equals(
- EXPECTED_AUTHOR_HTML, resp.get_data(as_text=True), ignore_blanks=True)
-
- def test_render_no_url(self):
- comment = copy.deepcopy(COMMENT)
- del comment['url']
- with self.request_context:
- Object(id='abc', as2=as2.from_as1(comment)).put()
-
- resp = self.client.get('/render?id=abc')
- self.assertEqual(200, resp.status_code)
- expected = EXPECTED_HTML.replace(
- '\n', ''
- ).replace('fake.com/123456', '')
- self.assert_multiline_equals(expected, resp.get_data(as_text=True),
- ignore_blanks=True)
-
- def test_render_deleted_object(self):
- with self.request_context:
- Object(id='abc', as2={'content': 'foo'}, deleted=True).put()
-
- resp = self.client.get('/render?id=abc')
- self.assertEqual(410, resp.status_code)
-
- def test_render_delete_activity(self):
- with self.request_context:
- Object(id='abc', as2=as2.from_as1(DELETE_OF_ID)).put()
-
- resp = self.client.get('/render?id=abc')
- self.assertEqual(410, resp.status_code)
-
- def test_render_update_inner_obj_exists_redirect(self):
- with self.request_context:
- # UPDATE's object field is a full object
- Object(id='abc', as2=as2.from_as1(UPDATE)).put()
- Object(id=UPDATE['object']['id'], as2={'content': 'foo'}).put()
-
- resp = self.client.get('/render?id=abc')
- self.assertEqual(301, resp.status_code)
- self.assertEqual(f'/render?id=tag%3Afake.com%3A123456',
- resp.headers['Location'])
-
- def test_render_delete_inner_obj_exists_redirect(self):
- with self.request_context:
- # DELETE_OF_ID's object field is a bare string id
- Object(id='abc', as2=as2.from_as1(DELETE_OF_ID)).put()
- Object(id=DELETE_OF_ID['object'], as2={'content': 'foo'}).put()
-
- resp = self.client.get('/render?id=abc')
- self.assertEqual(301, resp.status_code)
- self.assertEqual(f'/render?id=tag%3Afake.com%3A123456',
- resp.headers['Location'])
-
- def test_render_update_no_inner_obj_serve_as_is(self):
- with self.request_context:
- # UPDATE's object field is a full object
- Object(id='abc', as2=as2.from_as1(UPDATE)).put()
-
- resp = self.client.get('/render?id=abc')
- self.assertEqual(200, resp.status_code)
- self.assert_multiline_in("""\
-
-A ☕ reply
-
-
-""", resp.get_data(as_text=True), ignore_blanks=True)
-
- def test_render_update_inner_obj_too_minimal_serve_as_is(self):
- with self.request_context:
- # UPDATE's object field is a full object
- Object(id='abc', as2=as2.from_as1(UPDATE)).put()
- Object(id=UPDATE['object']['id'], as2={'id': 'foo'}).put()
-
- resp = self.client.get('/render?id=abc')
- self.assertEqual(200, resp.status_code)
- self.assert_multiline_in("""\
-
-A ☕ reply
-
-
-""", resp.get_data(as_text=True), ignore_blanks=True)
-