Add fediverse social features: followers/following lists, actor timelines
Adds get_followers_paginated and get_actor_timeline to FederationService protocol + SQL implementation + stubs. Includes accumulated federation changes: models, DTOs, delivery handler, webfinger, inline publishing, widget nav templates, and migration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -187,3 +187,68 @@ class APAnchorDTO:
|
||||
ots_proof_cid: str | None = None
|
||||
confirmed_at: datetime | None = None
|
||||
bitcoin_txid: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RemoteActorDTO:
|
||||
id: int
|
||||
actor_url: str
|
||||
inbox_url: str
|
||||
preferred_username: str
|
||||
domain: str
|
||||
display_name: str | None = None
|
||||
summary: str | None = None
|
||||
icon_url: str | None = None
|
||||
shared_inbox_url: str | None = None
|
||||
public_key_pem: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class RemotePostDTO:
|
||||
id: int
|
||||
remote_actor_id: int
|
||||
object_id: str
|
||||
content: str
|
||||
summary: str | None = None
|
||||
url: str | None = None
|
||||
attachments: list[dict] = field(default_factory=list)
|
||||
tags: list[dict] = field(default_factory=list)
|
||||
published: datetime | None = None
|
||||
actor: RemoteActorDTO | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TimelineItemDTO:
|
||||
id: str # composite key for cursor pagination
|
||||
post_type: str # "local" | "remote" | "boost"
|
||||
content: str # HTML
|
||||
published: datetime
|
||||
actor_name: str
|
||||
actor_username: str
|
||||
object_id: str | None = None
|
||||
summary: str | None = None
|
||||
url: str | None = None
|
||||
attachments: list[dict] = field(default_factory=list)
|
||||
tags: list[dict] = field(default_factory=list)
|
||||
actor_domain: str | None = None # None = local
|
||||
actor_icon: str | None = None
|
||||
actor_url: str | None = None
|
||||
boosted_by: str | None = None
|
||||
like_count: int = 0
|
||||
boost_count: int = 0
|
||||
liked_by_me: bool = False
|
||||
boosted_by_me: bool = False
|
||||
author_inbox: str | None = None
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class NotificationDTO:
|
||||
id: int
|
||||
notification_type: str # follow/like/boost/mention/reply
|
||||
from_actor_name: str
|
||||
from_actor_username: str
|
||||
created_at: datetime
|
||||
read: bool
|
||||
from_actor_domain: str | None = None
|
||||
from_actor_icon: str | None = None
|
||||
target_content_preview: str | None = None
|
||||
|
||||
@@ -22,6 +22,10 @@ from .dtos import (
|
||||
ActorProfileDTO,
|
||||
APActivityDTO,
|
||||
APFollowerDTO,
|
||||
RemoteActorDTO,
|
||||
RemotePostDTO,
|
||||
TimelineItemDTO,
|
||||
NotificationDTO,
|
||||
)
|
||||
|
||||
|
||||
@@ -217,6 +221,11 @@ class FederationService(Protocol):
|
||||
self, session: AsyncSession, username: str,
|
||||
) -> list[APFollowerDTO]: ...
|
||||
|
||||
async def get_followers_paginated(
|
||||
self, session: AsyncSession, username: str,
|
||||
page: int = 1, per_page: int = 20,
|
||||
) -> tuple[list[RemoteActorDTO], int]: ...
|
||||
|
||||
async def add_follower(
|
||||
self, session: AsyncSession, username: str,
|
||||
follower_acct: str, follower_inbox: str, follower_actor_url: str,
|
||||
@@ -227,5 +236,108 @@ class FederationService(Protocol):
|
||||
self, session: AsyncSession, username: str, follower_acct: str,
|
||||
) -> bool: ...
|
||||
|
||||
# -- Remote actors --------------------------------------------------------
|
||||
async def get_or_fetch_remote_actor(
|
||||
self, session: AsyncSession, actor_url: str,
|
||||
) -> RemoteActorDTO | None: ...
|
||||
|
||||
async def search_remote_actor(
|
||||
self, session: AsyncSession, acct: str,
|
||||
) -> RemoteActorDTO | None: ...
|
||||
|
||||
# -- Following (outbound) -------------------------------------------------
|
||||
async def send_follow(
|
||||
self, session: AsyncSession, local_username: str, remote_actor_url: str,
|
||||
) -> None: ...
|
||||
|
||||
async def get_following(
|
||||
self, session: AsyncSession, username: str,
|
||||
page: int = 1, per_page: int = 20,
|
||||
) -> tuple[list[RemoteActorDTO], int]: ...
|
||||
|
||||
async def accept_follow_response(
|
||||
self, session: AsyncSession, local_username: str, remote_actor_url: str,
|
||||
) -> None: ...
|
||||
|
||||
async def unfollow(
|
||||
self, session: AsyncSession, local_username: str, remote_actor_url: str,
|
||||
) -> None: ...
|
||||
|
||||
# -- Remote posts ---------------------------------------------------------
|
||||
async def ingest_remote_post(
|
||||
self, session: AsyncSession, remote_actor_id: int,
|
||||
activity_json: dict, object_json: dict,
|
||||
) -> None: ...
|
||||
|
||||
async def delete_remote_post(
|
||||
self, session: AsyncSession, object_id: str,
|
||||
) -> None: ...
|
||||
|
||||
async def get_remote_post(
|
||||
self, session: AsyncSession, object_id: str,
|
||||
) -> RemotePostDTO | None: ...
|
||||
|
||||
# -- Timelines ------------------------------------------------------------
|
||||
async def get_home_timeline(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
before: datetime | None = None, limit: int = 20,
|
||||
) -> list[TimelineItemDTO]: ...
|
||||
|
||||
async def get_public_timeline(
|
||||
self, session: AsyncSession,
|
||||
before: datetime | None = None, limit: int = 20,
|
||||
) -> list[TimelineItemDTO]: ...
|
||||
|
||||
async def get_actor_timeline(
|
||||
self, session: AsyncSession, remote_actor_id: int,
|
||||
before: datetime | None = None, limit: int = 20,
|
||||
) -> list[TimelineItemDTO]: ...
|
||||
|
||||
# -- Local posts ----------------------------------------------------------
|
||||
async def create_local_post(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
content: str, visibility: str = "public",
|
||||
in_reply_to: str | None = None,
|
||||
) -> int: ...
|
||||
|
||||
async def delete_local_post(
|
||||
self, session: AsyncSession, actor_profile_id: int, post_id: int,
|
||||
) -> None: ...
|
||||
|
||||
# -- Interactions ---------------------------------------------------------
|
||||
async def like_post(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
object_id: str, author_inbox: str,
|
||||
) -> None: ...
|
||||
|
||||
async def unlike_post(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
object_id: str, author_inbox: str,
|
||||
) -> None: ...
|
||||
|
||||
async def boost_post(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
object_id: str, author_inbox: str,
|
||||
) -> None: ...
|
||||
|
||||
async def unboost_post(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
object_id: str, author_inbox: str,
|
||||
) -> None: ...
|
||||
|
||||
# -- Notifications --------------------------------------------------------
|
||||
async def get_notifications(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
before: datetime | None = None, limit: int = 20,
|
||||
) -> list[NotificationDTO]: ...
|
||||
|
||||
async def unread_notification_count(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
) -> int: ...
|
||||
|
||||
async def mark_notifications_read(
|
||||
self, session: AsyncSession, actor_profile_id: int,
|
||||
) -> None: ...
|
||||
|
||||
# -- Stats ----------------------------------------------------------------
|
||||
async def get_stats(self, session: AsyncSession) -> dict: ...
|
||||
|
||||
Reference in New Issue
Block a user