diff options
| author | Dennis Fink | 2026-08-09 14:15:29 +0200 |
|---|---|---|
| committer | Dennis Fink | 2026-08-09 14:15:29 +0200 |
| commit | e4a8194e6de25e8a48e2c61ce7f89a8159fd99d9 (patch) | |
| tree | 234f63980aba8fe3c8d03e7bc8959598bce7e790 /webmentions_ssg/views.py | |
| parent | f53c1136184d2afabfb1e5974e527229aa71939a (diff) | |
| download | webmentions-ssg-e4a8194e6de25e8a48e2c61ce7f89a8159fd99d9.tar.gz webmentions-ssg-e4a8194e6de25e8a48e2c61ce7f89a8159fd99d9.zip | |
Implement received Webmention handling
Add the Flask application setup, database models and migrations,
authentication, and configuration for development and testing.
Implement asynchronous Webmention verification with Huey, including HTML
and plain-text source validation, retries, status tracking, and size
limits.
Add status, login, and paginated received-Webmention views together with
comprehensive tests for forms, views, and receiver tasks.
Diffstat (limited to '')
| -rw-r--r-- | webmentions_ssg/views.py | 160 |
1 files changed, 160 insertions, 0 deletions
diff --git a/webmentions_ssg/views.py b/webmentions_ssg/views.py new file mode 100644 index 0000000..5408fa4 --- /dev/null +++ b/webmentions_ssg/views.py @@ -0,0 +1,160 @@ +import uuid +from typing import cast +from urllib.parse import urlsplit + +import sqlalchemy as sa +import sqlalchemy.exc as sa_exc +from flask import ( + Blueprint, + Response, + abort, + flash, + redirect, + render_template, + request, + url_for, +) +from flask.typing import ResponseReturnValue +from flask_login import current_user, login_required, login_user, logout_user + +from . import DATABASE as db +from . import forms +from .models import ReceivedWebmention, User +from .tasks.receiver import verify_webmention + +root_page = Blueprint("root", __name__) + + +@root_page.route("/") +def index() -> ResponseReturnValue: + return redirect(url_for("root.received")) + + +@root_page.route("/login", methods=["GET", "POST"]) +def login() -> ResponseReturnValue: + if current_user.is_authenticated: + return redirect(url_for("root.index")) + + form = forms.LoginForm() + + if form.validate_on_submit(): + user = db.session.scalar( + sa.select(User).where(User.username == form.username.data) + ) + + if user is None or not user.check_password(form.password.data): + flash("Invalid username or password", "danger") + return redirect(url_for("root.login")) + + login_user(user) + + next_page = request.args.get("next") + if not next_page or urlsplit(next_page).netloc != "": + next_page = url_for("root.index") + return redirect(next_page) + + return render_template("login.html", form=form) + + +@root_page.route("/logout") +def logout() -> ResponseReturnValue: + logout_user() + return redirect(url_for("root.index")) + + +@root_page.route("/received") +@login_required +def received() -> ResponseReturnValue: + webmentions = db.paginate( + sa.select(ReceivedWebmention).order_by(ReceivedWebmention.uuid.desc()), + per_page=25, + ) + + return render_template( + "received.html", + title="Received Webmentions", + webmentions=webmentions, + ) + + +@root_page.route("/endpoint", methods=["POST"]) +def endpoint() -> ResponseReturnValue: + form = forms.EndpointForm(meta={"csrf": False}) + + if not form.validate_on_submit(): + return form.errors, 400 + + source = cast(str, form.source.data) + target = cast(str, form.target.data) + + webmention = db.session.execute( + sa.select(ReceivedWebmention).where( + ReceivedWebmention.source == source, + ReceivedWebmention.target == target, + ) + ).scalar_one_or_none() + + if webmention is None: + identifier = uuid.uuid7() + + webmention = ReceivedWebmention( + uuid=identifier, + source=source, + target=target, + ) + + db.session.add(webmention) + + try: + db.session.commit() + except sa_exc.IntegrityError: + db.session.rollback() + + # Another request may have inserted the same source/target pair + # after our SELECT but before our COMMIT. + webmention = db.session.execute( + sa.select(ReceivedWebmention).where( + ReceivedWebmention.source == source, + ReceivedWebmention.target == target, + ) + ).scalar_one() + + identifier = webmention.uuid + webmention.status = "received" + webmention.failure_reason = None + + db.session.commit() + + else: + identifier = webmention.uuid + + # Re-sent webmention: re-verify the existing row. + webmention.status = "received" + webmention.failure_reason = None + + db.session.commit() + + verify_webmention(webmention.uuid) + + status_url = url_for( + "root.status", + identifier=str(identifier), + _external=True, + ) + + return Response( + status=201, + headers={"Location": status_url}, + ) + + +@root_page.route("/status/<uuid:identifier>") +def status(identifier: uuid.UUID) -> ResponseReturnValue: + webmention = db.session.get(ReceivedWebmention, identifier) + + if webmention is None: + return abort(404) + + return render_template( + "status.html", title="Webmention status", webmention=webmention + ) |
