diff --git a/app.py b/app.py index e2d935d..b0cb623 100644 --- a/app.py +++ b/app.py @@ -63,9 +63,10 @@ from core.meta import is_public from core.meta import not_undo from core.shared import _build_thread from core.shared import _get_ip +from core.shared import activitypubify from core.shared import csrf +from core.shared import htmlify from core.shared import is_api_request -from core.shared import jsonify from core.shared import login_required from core.shared import noindex from core.shared import paginated_query @@ -322,7 +323,7 @@ def serve_uploads(oid, fname): def remote_follow(): """Form to allow visitor to perform the remote follow dance.""" if request.method == "GET": - return render_template("remote_follow.html") + return htmlify(render_template("remote_follow.html")) csrf.protect() profile = request.form.get("profile") @@ -339,8 +340,7 @@ def remote_follow(): def index(): if is_api_request(): _log_sig() - print(ME) - return jsonify(**ME) + return activitypubify(**ME) q = { "box": Box.OUTBOX.value, @@ -369,14 +369,15 @@ def index(): DB.activities, q, limit=25 - len(pinned) ) - resp = render_template( - "index.html", - outbox_data=outbox_data, - older_than=older_than, - newer_than=newer_than, - pinned=pinned, + return htmlify( + render_template( + "index.html", + outbox_data=outbox_data, + older_than=older_than, + newer_than=newer_than, + pinned=pinned, + ) ) - return resp @app.route("/all") @@ -391,11 +392,13 @@ def all(): } outbox_data, older_than, newer_than = paginated_query(DB.activities, q) - return render_template( - "index.html", - outbox_data=outbox_data, - older_than=older_than, - newer_than=newer_than, + return htmlify( + render_template( + "index.html", + outbox_data=outbox_data, + older_than=older_than, + newer_than=newer_than, + ) ) @@ -458,8 +461,10 @@ def note_by_id(note_id): app.logger.exception(f"invalid doc: {doc!r}") app.logger.info(f"shares={shares!r}") - return render_template( - "note.html", likes=likes, shares=shares, thread=thread, note=data + return htmlify( + render_template( + "note.html", likes=likes, shares=shares, thread=thread, note=data + ) ) @@ -477,7 +482,7 @@ def outbox(): "meta.public": True, "type": {"$in": [ActivityType.CREATE.value, ActivityType.ANNOUNCE.value]}, } - return jsonify( + return activitypubify( **activitypub.build_ordered_collection( DB.activities, q=q, @@ -503,7 +508,9 @@ def outbox(): @app.route("/emoji/") def ap_emoji(name): if name in EMOJIS: - return jsonify(**{**EMOJIS[name].to_dict(), "@context": config.DEFAULT_CTX}) + return activitypubify( + **{**EMOJIS[name].to_dict(), "@context": config.DEFAULT_CTX} + ) abort(404) @@ -523,7 +530,7 @@ def outbox_detail(item_id): if doc["meta"].get("deleted", False): abort(404) - return jsonify(**activity_from_doc(doc)) + return activitypubify(**activity_from_doc(doc)) @app.route("/outbox//activity") @@ -541,7 +548,7 @@ def outbox_activity(item_id): if obj["type"] != ActivityType.CREATE.value: abort(404) - return jsonify(**obj["object"]) + return activitypubify(**obj["object"]) @app.route("/outbox//replies") @@ -570,7 +577,7 @@ def outbox_activity_replies(item_id): "activity.object.inReplyTo": obj.get_object().id, } - return jsonify( + return activitypubify( **activitypub.build_ordered_collection( DB.activities, q=q, @@ -610,7 +617,7 @@ def outbox_activity_likes(item_id): ], } - return jsonify( + return activitypubify( **activitypub.build_ordered_collection( DB.activities, q=q, @@ -649,7 +656,7 @@ def outbox_activity_shares(item_id): ], } - return jsonify( + return activitypubify( **activitypub.build_ordered_collection( DB.activities, q=q, @@ -672,7 +679,7 @@ def inbox(): except BadSignature: abort(404) - return jsonify( + return activitypubify( **activitypub.build_ordered_collection( DB.activities, q={"meta.deleted": False, "box": Box.INBOX.value}, @@ -800,7 +807,7 @@ def followers(): if is_api_request(): _log_sig() - return jsonify( + return activitypubify( **activitypub.build_ordered_collection( DB.activities, q=q, @@ -814,11 +821,13 @@ def followers(): followers = [ doc["meta"]["actor"] for doc in raw_followers if "actor" in doc.get("meta", {}) ] - return render_template( - "followers.html", - followers_data=followers, - older_than=older_than, - newer_than=newer_than, + return htmlify( + render_template( + "followers.html", + followers_data=followers, + older_than=older_than, + newer_than=newer_than, + ) ) @@ -829,11 +838,11 @@ def following(): if is_api_request(): _log_sig() if config.HIDE_FOLLOWING: - return jsonify( + return activitypubify( **activitypub.simple_build_ordered_collection("following", []) ) - return jsonify( + return activitypubify( **activitypub.build_ordered_collection( DB.activities, q=q, @@ -853,12 +862,14 @@ def following(): if "remote_id" in doc and "object" in doc.get("meta", {}) ] lists = list(DB.lists.find()) - return render_template( - "following.html", - following_data=following, - older_than=older_than, - newer_than=newer_than, - lists=lists, + return htmlify( + render_template( + "following.html", + following_data=following, + older_than=older_than, + newer_than=newer_than, + lists=lists, + ) ) @@ -873,18 +884,20 @@ def tags(tag): ): abort(404) if not is_api_request(): - return render_template( - "tags.html", - tag=tag, - outbox_data=DB.activities.find( - { - "box": Box.OUTBOX.value, - "type": ActivityType.CREATE.value, - "meta.deleted": False, - "activity.object.tag.type": "Hashtag", - "activity.object.tag.name": "#" + tag, - } - ), + return htmlify( + render_template( + "tags.html", + tag=tag, + outbox_data=DB.activities.find( + { + "box": Box.OUTBOX.value, + "type": ActivityType.CREATE.value, + "meta.deleted": False, + "activity.object.tag.type": "Hashtag", + "activity.object.tag.name": "#" + tag, + } + ), + ) ) _log_sig() q = { @@ -895,7 +908,7 @@ def tags(tag): "activity.object.tag.type": "Hashtag", "activity.object.tag.name": "#" + tag, } - return jsonify( + return activitypubify( **activitypub.build_ordered_collection( DB.activities, q=q, @@ -920,7 +933,9 @@ def featured(): "meta.pinned": True, } data = [clean_activity(doc["activity"]["object"]) for doc in DB.activities.find(q)] - return jsonify(**activitypub.simple_build_ordered_collection("featured", data)) + return activitypubify( + **activitypub.simple_build_ordered_collection("featured", data) + ) @app.route("/liked") @@ -936,12 +951,14 @@ def liked(): liked, older_than, newer_than = paginated_query(DB.activities, q) - return render_template( - "liked.html", liked=liked, older_than=older_than, newer_than=newer_than + return htmlify( + render_template( + "liked.html", liked=liked, older_than=older_than, newer_than=newer_than + ) ) q = {"meta.deleted": False, "meta.undo": False, "type": ActivityType.LIKE.value} - return jsonify( + return activitypubify( **activitypub.build_ordered_collection( DB.activities, q=q, diff --git a/blueprints/admin.py b/blueprints/admin.py index ec87b6e..3bae40e 100644 --- a/blueprints/admin.py +++ b/blueprints/admin.py @@ -32,6 +32,7 @@ from core.shared import MY_PERSON from core.shared import _build_thread from core.shared import _Response from core.shared import csrf +from core.shared import htmlify from core.shared import login_required from core.shared import noindex from core.shared import p @@ -113,7 +114,9 @@ def admin_login() -> _Response: payload = u2f.begin_authentication(ID, devices) session["challenge"] = payload - return render_template("login.html", u2f_enabled=u2f_enabled, payload=payload) + return htmlify( + render_template("login.html", u2f_enabled=u2f_enabled, payload=payload) + ) @blueprint.route("/admin", methods=["GET"]) @@ -127,47 +130,53 @@ def admin_index() -> _Response: } col_liked = DB.activities.count(q) - return render_template( - "admin.html", - instances=list(DB.instances.find()), - inbox_size=DB.activities.count({"box": Box.INBOX.value}), - outbox_size=DB.activities.count({"box": Box.OUTBOX.value}), - col_liked=col_liked, - col_followers=DB.activities.count( - { - "box": Box.INBOX.value, - "type": ap.ActivityType.FOLLOW.value, - "meta.undo": False, - } - ), - col_following=DB.activities.count( - { - "box": Box.OUTBOX.value, - "type": ap.ActivityType.FOLLOW.value, - "meta.undo": False, - } - ), + return htmlify( + render_template( + "admin.html", + instances=list(DB.instances.find()), + inbox_size=DB.activities.count({"box": Box.INBOX.value}), + outbox_size=DB.activities.count({"box": Box.OUTBOX.value}), + col_liked=col_liked, + col_followers=DB.activities.count( + { + "box": Box.INBOX.value, + "type": ap.ActivityType.FOLLOW.value, + "meta.undo": False, + } + ), + col_following=DB.activities.count( + { + "box": Box.OUTBOX.value, + "type": ap.ActivityType.FOLLOW.value, + "meta.undo": False, + } + ), + ) ) @blueprint.route("/admin/indieauth", methods=["GET"]) @login_required def admin_indieauth() -> _Response: - return render_template( - "admin_indieauth.html", - indieauth_actions=DB.indieauth.find().sort("ts", -1).limit(100), + return htmlify( + render_template( + "admin_indieauth.html", + indieauth_actions=DB.indieauth.find().sort("ts", -1).limit(100), + ) ) @blueprint.route("/admin/tasks", methods=["GET"]) @login_required def admin_tasks() -> _Response: - return render_template( - "admin_tasks.html", - success=p.get_success(), - dead=p.get_dead(), - waiting=p.get_waiting(), - cron=p.get_cron(), + return htmlify( + render_template( + "admin_tasks.html", + success=p.get_success(), + dead=p.get_dead(), + waiting=p.get_waiting(), + cron=p.get_cron(), + ) ) @@ -191,8 +200,10 @@ def admin_lookup() -> _Response: print(data) app.logger.debug(data.to_dict()) - return render_template( - "lookup.html", data=data, meta=meta, url=request.args.get("url") + return htmlify( + render_template( + "lookup.html", data=data, meta=meta, url=request.args.get("url") + ) ) @@ -214,7 +225,7 @@ def admin_thread() -> _Response: tpl = "note.html" if request.args.get("debug"): tpl = "note_debug.html" - return render_template(tpl, thread=thread, note=data) + return htmlify(render_template(tpl, thread=thread, note=data)) @blueprint.route("/admin/new", methods=["GET"]) @@ -246,14 +257,16 @@ def admin_new() -> _Response: content = f"@{actor.preferredUsername}@{domain} " thread = _build_thread(data) - return render_template( - "new.html", - reply=reply_id, - content=content, - thread=thread, - visibility=ap.Visibility, - emojis=config.EMOJIS.split(" "), - custom_emojis=EMOJIS_BY_NAME, + return htmlify( + render_template( + "new.html", + reply=reply_id, + content=content, + thread=thread, + visibility=ap.Visibility, + emojis=config.EMOJIS.split(" "), + custom_emojis=EMOJIS_BY_NAME, + ) ) @@ -262,7 +275,7 @@ def admin_new() -> _Response: def admin_lists() -> _Response: lists = list(DB.lists.find()) - return render_template("lists.html", lists=lists) + return htmlify(render_template("lists.html", lists=lists)) @blueprint.route("/admin/notifications") @@ -341,12 +354,14 @@ def admin_notifications() -> _Response: inbox_data, reverse=True, key=lambda doc: doc["_id"].generation_time ) - return render_template( - "stream.html", - inbox_data=inbox_data, - older_than=older_than, - newer_than=newer_than, - nid=nid, + return htmlify( + render_template( + "stream.html", + inbox_data=inbox_data, + older_than=older_than, + newer_than=newer_than, + nid=nid, + ) ) @@ -365,8 +380,10 @@ def admin_stream() -> _Response: DB.activities, q, limit=int(request.args.get("limit", 25)) ) - return render_template( - tpl, inbox_data=inbox_data, older_than=older_than, newer_than=newer_than + return htmlify( + render_template( + tpl, inbox_data=inbox_data, older_than=older_than, newer_than=newer_than + ) ) @@ -393,8 +410,10 @@ def admin_list(name: str) -> _Response: DB.activities, q, limit=int(request.args.get("limit", 25)) ) - return render_template( - tpl, inbox_data=inbox_data, older_than=older_than, newer_than=newer_than + return htmlify( + render_template( + tpl, inbox_data=inbox_data, older_than=older_than, newer_than=newer_than + ) ) @@ -413,8 +432,10 @@ def admin_bookmarks() -> _Response: DB.activities, q, limit=int(request.args.get("limit", 25)) ) - return render_template( - tpl, inbox_data=inbox_data, older_than=older_than, newer_than=newer_than + return htmlify( + render_template( + tpl, inbox_data=inbox_data, older_than=older_than, newer_than=newer_than + ) ) @@ -425,7 +446,7 @@ def u2f_register(): if request.method == "GET": payload = u2f.begin_registration(ID) session["challenge"] = payload - return render_template("u2f.html", payload=payload) + return htmlify(render_template("u2f.html", payload=payload)) else: resp = json.loads(request.form.get("resp")) device, device_cert = u2f.complete_registration(session["challenge"], resp) @@ -439,8 +460,10 @@ def u2f_register(): @login_required def authorize_follow(): if request.method == "GET": - return render_template( - "authorize_remote_follow.html", profile=request.args.get("profile") + return htmlify( + render_template( + "authorize_remote_follow.html", profile=request.args.get("profile") + ) ) actor = get_actor_url(request.form.get("profile")) diff --git a/blueprints/indieauth.py b/blueprints/indieauth.py index 9514ec3..08188e5 100644 --- a/blueprints/indieauth.py +++ b/blueprints/indieauth.py @@ -19,6 +19,7 @@ from itsdangerous import BadSignature from config import DB from config import JWT from core.shared import _get_ip +from core.shared import htmlify from core.shared import login_required blueprint = flask.Blueprint("indieauth", __name__) @@ -105,15 +106,17 @@ def indieauth_endpoint(): scope = request.args.get("scope", "").split() print("STATE", state) - return render_template( - "indieauth_flow.html", - client=get_client_id_data(client_id), - scopes=scope, - redirect_uri=redirect_uri, - state=state, - response_type=response_type, - client_id=client_id, - me=me, + return htmlify( + render_template( + "indieauth_flow.html", + client=get_client_id_data(client_id), + scopes=scope, + redirect_uri=redirect_uri, + state=state, + response_type=response_type, + client_id=client_id, + me=me, + ) ) # Auth verification via POST diff --git a/blueprints/tasks.py b/blueprints/tasks.py index d2545dc..950e4c8 100644 --- a/blueprints/tasks.py +++ b/blueprints/tasks.py @@ -554,10 +554,14 @@ def task_process_reply() -> _Response: root_reply = in_reply_to + # Fetch the activity reply reply = ap.fetch_remote_activity(in_reply_to) if reply.has_type(ap.ActivityType.CREATE): reply = reply.get_object() + # Store some metadata for the UI + # FIXME(tsileo): be able to display: "In reply to @user@domain.tld"? + new_replies = [activity, reply] while 1: diff --git a/config.py b/config.py index 71523eb..3f8e0f7 100644 --- a/config.py +++ b/config.py @@ -8,7 +8,7 @@ from pathlib import Path import yaml from itsdangerous import JSONWebSignatureSerializer from little_boxes import strtobool -from little_boxes.activitypub import DEFAULT_CTX as AP_DEFAULT_CTX +from little_boxes.activitypub import CTX_AS as AP_DEFAULT_CTX from pymongo import MongoClient import sass diff --git a/core/activitypub.py b/core/activitypub.py index 2559f71..9cebffe 100644 --- a/core/activitypub.py +++ b/core/activitypub.py @@ -263,6 +263,9 @@ def post_to_outbox(activity: ap.BaseActivity) -> str: class MicroblogPubBackend(Backend): """Implements a Little Boxes backend, backed by MongoDB.""" + def ap_context(self) -> Any: + return DEFAULT_CTX + def base_url(self) -> str: return BASE_URL diff --git a/core/shared.py b/core/shared.py index 5ea6db9..77702ad 100644 --- a/core/shared.py +++ b/core/shared.py @@ -59,7 +59,19 @@ def build_resp(resp): return resp, headers -def jsonify(**data): +def htmlify(data): + resp, headers = build_resp(data) + return Response( + response=resp, + headers={ + **headers, + "Content-Type": "text/html; charset=utf-8", + "Cache-Control": "max-age=0, private, must-revalidate", + }, + ) + + +def activitypubify(**data): if "@context" not in data: data["@context"] = config.DEFAULT_CTX resp, headers = build_resp(json.dumps(data)) @@ -68,9 +80,7 @@ def jsonify(**data): headers={ **headers, "Cache-Control": "max-age=0, private, must-revalidate", - "Content-Type": "application/json" - if app.debug - else "application/activity+json", + "Content-Type": "application/activity+json", }, )