From 67eff0a854da010cb6ecd119d84238ec3e119272 Mon Sep 17 00:00:00 2001 From: Dennis Fink Date: Sun, 9 Aug 2026 14:15:29 +0200 Subject: 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. --- webmentions_ssg/models.py | 89 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 webmentions_ssg/models.py (limited to 'webmentions_ssg/models.py') diff --git a/webmentions_ssg/models.py b/webmentions_ssg/models.py new file mode 100644 index 0000000..8c378aa --- /dev/null +++ b/webmentions_ssg/models.py @@ -0,0 +1,89 @@ +import uuid +from datetime import datetime, timezone +from typing import Optional + +from flask_login import UserMixin +from sqlalchemy import DateTime, String, Text, UniqueConstraint, Uuid +from sqlalchemy.orm import Mapped, mapped_column +from werkzeug.security import check_password_hash, generate_password_hash + +from . import Base + + +class User(UserMixin, Base): + __tablename__ = "users" + + id: Mapped[int] = mapped_column(primary_key=True) + username: Mapped[str] = mapped_column( + String(64), + index=True, + unique=True, + ) + password_hash: Mapped[Optional[str]] = mapped_column(String(256)) + + def __repr__(self): + return f"" + + def set_password(self, password): + self.password_hash = generate_password_hash(password) + + def check_password(self, password): + return check_password_hash( + self.password_hash, + password, + ) + + +class ReceivedWebmention(Base): + __tablename__ = "received_webmentions" + + uuid: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), + primary_key=True, + ) + + source: Mapped[str] = mapped_column( + Text(), + nullable=False, + ) + target: Mapped[str] = mapped_column( + Text(), + nullable=False, + ) + + status: Mapped[str] = mapped_column( + Text(), + nullable=False, + default="received", + ) + failure_reason: Mapped[str | None] = mapped_column( + Text(), + nullable=True, + default=None, + ) + + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=lambda: datetime.now(timezone.utc), + onupdate=lambda: datetime.now(timezone.utc), + nullable=False, + ) + + __table_args__ = ( + UniqueConstraint( + "source", + "target", + name="uq_webmention_source_target", + ), + ) + + @property + def verified(self) -> bool: + return self.status == "verified" + + @property + def created_at(self) -> datetime: + return datetime.fromtimestamp( + self.uuid.time / 1000, + tz=timezone.utc, + ) -- cgit v1.3.1