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/") 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 )