Add fediverse social tables, protocols, and implementations
6 new ORM models (remote actors, following, remote posts, local posts, interactions, notifications), 20 new FederationService methods with SQL implementations and stubs, WebFinger client, and Alembic migration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,4 +29,5 @@ from .container_relation import ContainerRelation
|
||||
from .menu_node import MenuNode
|
||||
from .federation import (
|
||||
ActorProfile, APActivity, APFollower, APInboxItem, APAnchor, IPFSPin,
|
||||
RemoteActor, APFollowing, APRemotePost, APLocalPost, APInteraction, APNotification,
|
||||
)
|
||||
|
||||
@@ -193,3 +193,207 @@ class IPFSPin(Base):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<IPFSPin {self.id} {self.ipfs_cid[:16]}...>"
|
||||
|
||||
|
||||
class RemoteActor(Base):
|
||||
"""Cached profile of a remote actor we interact with."""
|
||||
__tablename__ = "ap_remote_actors"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
actor_url: Mapped[str] = mapped_column(String(512), unique=True, nullable=False)
|
||||
inbox_url: Mapped[str] = mapped_column(String(512), nullable=False)
|
||||
shared_inbox_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
preferred_username: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
display_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
summary: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
icon_url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
public_key_pem: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
domain: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
fetched_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_ap_remote_actor_url", "actor_url", unique=True),
|
||||
Index("ix_ap_remote_actor_domain", "domain"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<RemoteActor {self.id} {self.preferred_username}@{self.domain}>"
|
||||
|
||||
|
||||
class APFollowing(Base):
|
||||
"""Outbound follow: local actor → remote actor."""
|
||||
__tablename__ = "ap_following"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
actor_profile_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=False,
|
||||
)
|
||||
remote_actor_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("ap_remote_actors.id", ondelete="CASCADE"), nullable=False,
|
||||
)
|
||||
state: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="pending", server_default="pending",
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
||||
)
|
||||
accepted_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
# Relationships
|
||||
actor_profile = relationship("ActorProfile")
|
||||
remote_actor = relationship("RemoteActor")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("actor_profile_id", "remote_actor_id", name="uq_following"),
|
||||
Index("ix_ap_following_actor", "actor_profile_id"),
|
||||
Index("ix_ap_following_remote", "remote_actor_id"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<APFollowing {self.id} [{self.state}]>"
|
||||
|
||||
|
||||
class APRemotePost(Base):
|
||||
"""A federated post ingested from a remote actor."""
|
||||
__tablename__ = "ap_remote_posts"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
remote_actor_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("ap_remote_actors.id", ondelete="CASCADE"), nullable=False,
|
||||
)
|
||||
activity_id: Mapped[str] = mapped_column(String(512), unique=True, nullable=False)
|
||||
object_id: Mapped[str] = mapped_column(String(512), unique=True, nullable=False)
|
||||
object_type: Mapped[str] = mapped_column(String(64), nullable=False, default="Note")
|
||||
content: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
summary: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||
url: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
attachment_data: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
tag_data: Mapped[dict | None] = mapped_column(JSONB, nullable=True)
|
||||
in_reply_to: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
conversation: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
published: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
fetched_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
remote_actor = relationship("RemoteActor")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_ap_remote_post_actor", "remote_actor_id"),
|
||||
Index("ix_ap_remote_post_published", "published"),
|
||||
Index("ix_ap_remote_post_object", "object_id", unique=True),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<APRemotePost {self.id} {self.object_type}>"
|
||||
|
||||
|
||||
class APLocalPost(Base):
|
||||
"""A native post composed in the federation UI."""
|
||||
__tablename__ = "ap_local_posts"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
actor_profile_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=False,
|
||||
)
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
visibility: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="public", server_default="public",
|
||||
)
|
||||
in_reply_to: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
published: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
||||
)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now(),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
actor_profile = relationship("ActorProfile")
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_ap_local_post_actor", "actor_profile_id"),
|
||||
Index("ix_ap_local_post_published", "published"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<APLocalPost {self.id}>"
|
||||
|
||||
|
||||
class APInteraction(Base):
|
||||
"""Like or boost (local or remote)."""
|
||||
__tablename__ = "ap_interactions"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
actor_profile_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=True,
|
||||
)
|
||||
remote_actor_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("ap_remote_actors.id", ondelete="CASCADE"), nullable=True,
|
||||
)
|
||||
post_type: Mapped[str] = mapped_column(String(20), nullable=False) # local/remote
|
||||
post_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
interaction_type: Mapped[str] = mapped_column(String(20), nullable=False) # like/boost
|
||||
activity_id: Mapped[str | None] = mapped_column(String(512), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
||||
)
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_ap_interaction_post", "post_type", "post_id"),
|
||||
Index("ix_ap_interaction_actor", "actor_profile_id"),
|
||||
Index("ix_ap_interaction_remote", "remote_actor_id"),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<APInteraction {self.id} {self.interaction_type}>"
|
||||
|
||||
|
||||
class APNotification(Base):
|
||||
"""Notification for a local actor."""
|
||||
__tablename__ = "ap_notifications"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
|
||||
actor_profile_id: Mapped[int] = mapped_column(
|
||||
Integer, ForeignKey("ap_actor_profiles.id", ondelete="CASCADE"), nullable=False,
|
||||
)
|
||||
notification_type: Mapped[str] = mapped_column(String(20), nullable=False)
|
||||
from_remote_actor_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("ap_remote_actors.id", ondelete="SET NULL"), nullable=True,
|
||||
)
|
||||
from_actor_profile_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("ap_actor_profiles.id", ondelete="SET NULL"), nullable=True,
|
||||
)
|
||||
target_activity_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("ap_activities.id", ondelete="SET NULL"), nullable=True,
|
||||
)
|
||||
target_remote_post_id: Mapped[int | None] = mapped_column(
|
||||
Integer, ForeignKey("ap_remote_posts.id", ondelete="SET NULL"), nullable=True,
|
||||
)
|
||||
read: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False, server_default="false")
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(),
|
||||
)
|
||||
|
||||
# Relationships
|
||||
actor_profile = relationship("ActorProfile", foreign_keys=[actor_profile_id])
|
||||
from_remote_actor = relationship("RemoteActor")
|
||||
from_actor_profile = relationship("ActorProfile", foreign_keys=[from_actor_profile_id])
|
||||
|
||||
__table_args__ = (
|
||||
Index("ix_ap_notification_actor", "actor_profile_id"),
|
||||
Index("ix_ap_notification_read", "actor_profile_id", "read"),
|
||||
Index("ix_ap_notification_created", "created_at"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user