diff options
| author | Dennis Fink | 2026-08-21 11:08:53 +0200 |
|---|---|---|
| committer | Dennis Fink | 2026-08-21 11:08:53 +0200 |
| commit | 66ad7fefff6edc1d993f5c347430a28d63727988 (patch) | |
| tree | 3fd2d66d0505628480692d5c8ad86131c657be83 /webmentions_ssg/views.py | |
| parent | b07b61a354b32637acf84cec2c5b4f8019cb94b7 (diff) | |
| download | webmentions-ssg-66ad7fefff6edc1d993f5c347430a28d63727988.tar.gz webmentions-ssg-66ad7fefff6edc1d993f5c347430a28d63727988.zip | |
feat(sender): add manual Webmention resend action
Add an authenticated action to reopen a sent Webmention revision and
queue it for delivery again from the source detail view.
Clear the previous attempt state while preserving historical
successful-send data, and show pending Webmentions explicitly in the UI.
Diffstat (limited to '')
| -rw-r--r-- | webmentions_ssg/views.py | 58 |
1 files changed, 53 insertions, 5 deletions
diff --git a/webmentions_ssg/views.py b/webmentions_ssg/views.py index 3430cea..2235735 100644 --- a/webmentions_ssg/views.py +++ b/webmentions_ssg/views.py @@ -24,9 +24,10 @@ from sqlalchemy.orm import selectinload from . import CSRF, forms from . import DATABASE as db -from .models import ReceivedWebmention, Source, User +from .models import ReceivedWebmention, SentWebmention, Source, User from .tasks.receiver import verify_webmention from .tasks.scanner import manual_scan_sources +from .tasks.sender import send_webmention root_page = Blueprint("root", __name__) @@ -209,19 +210,19 @@ def rescan_sent_sources() -> ResponseReturnValue: return redirect(url_for("root.sent")) -@root_page.route("/sent/<uuid:identifier>") +@root_page.route("/sent/<uuid:source_identifier>") @login_required -def sent_source(identifier: uuid.UUID) -> ResponseReturnValue: +def sent_source(source_identifier: uuid.UUID) -> ResponseReturnValue: """ Display the sent Webmentions associated with a source. - :param identifier: Identifier of the source to display. + :param source_identifier: Identifier of the source to display. :return: Rendered source details page. """ source = db.session.scalar( sa.select(Source) .options(selectinload(Source.sent_webmentions)) - .where(Source.uuid == identifier) + .where(Source.uuid == source_identifier) ) if source is None: return abort(404) @@ -234,9 +235,56 @@ def sent_source(identifier: uuid.UUID) -> ResponseReturnValue: title="Sent Webmentions", source=source, webmentions=webmentions, + action_form=forms.AdminActionForm(), ) +@root_page.post("/sent/<uuid:source_identifier>/<uuid:webmention_identifier>/resend") +@login_required +def resend_sent_webmention( + source_identifier: uuid.UUID, webmention_identifier: uuid.UUID +) -> ResponseReturnValue: + """ + Queue an outgoing Webmention to be sent again. + + The current desired source revision is reopened for processing so the + sender task does not treat the Webmention as already completed. Historical + successful-send fields are preserved, while result fields from the previous + attempt are cleared. + + :param source_identifier: Identifier of the source owning the Webmention. + :param webmention_identifier: Identifier of the sent Webmention to resend. + :return: Redirect response to the source details view. + """ + form = forms.AdminActionForm() + + if not form.validate_on_submit(): + return abort(400) + + webmention = db.session.get(SentWebmention, webmention_identifier) + + if webmention is None or webmention.source_id != source_identifier: + return abort(404) + + # Reopen the existing desired revision instead of inventing a new source + # revision. send_webmention() otherwise exits when this revision has already + # been processed. + webmention.processed_revision = None + webmention.status = None + webmention.failure_reason = None + webmention.endpoint = None + webmention.response_status = None + webmention.status_url = None + + db.session.commit() + + send_webmention(webmention_identifier) + + flash(f"Webmention {webmention.uuid} queued for resend.", "success") + + return redirect(url_for("root.sent_source", identifier=source_identifier)) + + @root_page.post("/endpoint") @CSRF.exempt def endpoint() -> ResponseReturnValue: |
