aboutsummaryrefslogtreecommitdiff
path: root/webmentions_ssg/views.py
diff options
context:
space:
mode:
authorDennis Fink2026-08-21 11:08:53 +0200
committerDennis Fink2026-08-21 11:08:53 +0200
commit66ad7fefff6edc1d993f5c347430a28d63727988 (patch)
tree3fd2d66d0505628480692d5c8ad86131c657be83 /webmentions_ssg/views.py
parentb07b61a354b32637acf84cec2c5b4f8019cb94b7 (diff)
downloadwebmentions-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.py58
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: