Add search_actors to FederationService for paginated actor search
Fuzzy ILIKE search across remote actors and local profiles, with WebFinger resolution for @user@domain queries. Supports page-based pagination for infinite scroll. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -262,6 +262,10 @@ class FederationService(Protocol):
|
||||
self, session: AsyncSession, acct: str,
|
||||
) -> RemoteActorDTO | None: ...
|
||||
|
||||
async def search_actors(
|
||||
self, session: AsyncSession, query: str, page: int = 1, limit: int = 20,
|
||||
) -> tuple[list[RemoteActorDTO], int]: ...
|
||||
|
||||
# -- Following (outbound) -------------------------------------------------
|
||||
async def send_follow(
|
||||
self, session: AsyncSession, local_username: str, remote_actor_url: str,
|
||||
|
||||
@@ -541,6 +541,92 @@ class SqlFederationService:
|
||||
|
||||
return await self._upsert_remote_actor(session, actor_url, data)
|
||||
|
||||
async def search_actors(
|
||||
self, session: AsyncSession, query: str, page: int = 1, limit: int = 20,
|
||||
) -> tuple[list[RemoteActorDTO], int]:
|
||||
from sqlalchemy import or_
|
||||
|
||||
pattern = f"%{query}%"
|
||||
offset = (page - 1) * limit
|
||||
|
||||
# WebFinger resolve for @user@domain queries (first page only)
|
||||
webfinger_result: RemoteActorDTO | None = None
|
||||
if page == 1 and "@" in query:
|
||||
webfinger_result = await self.search_remote_actor(session, query)
|
||||
|
||||
# Search cached remote actors
|
||||
remote_filter = or_(
|
||||
RemoteActor.preferred_username.ilike(pattern),
|
||||
RemoteActor.display_name.ilike(pattern),
|
||||
RemoteActor.domain.ilike(pattern),
|
||||
)
|
||||
remote_total = (
|
||||
await session.execute(
|
||||
select(func.count(RemoteActor.id)).where(remote_filter)
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
# Search local actor profiles
|
||||
local_filter = or_(
|
||||
ActorProfile.preferred_username.ilike(pattern),
|
||||
ActorProfile.display_name.ilike(pattern),
|
||||
)
|
||||
local_total = (
|
||||
await session.execute(
|
||||
select(func.count(ActorProfile.id)).where(local_filter)
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
total = remote_total + local_total
|
||||
|
||||
# Fetch remote actors page
|
||||
remote_rows = (
|
||||
await session.execute(
|
||||
select(RemoteActor)
|
||||
.where(remote_filter)
|
||||
.order_by(RemoteActor.preferred_username)
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
).scalars().all()
|
||||
|
||||
results: list[RemoteActorDTO] = [_remote_actor_to_dto(r) for r in remote_rows]
|
||||
|
||||
# Fill remaining slots with local actors
|
||||
remaining = limit - len(results)
|
||||
local_offset = max(0, offset - remote_total)
|
||||
if remaining > 0 and offset + len(results) >= remote_total:
|
||||
domain = _domain()
|
||||
local_rows = (
|
||||
await session.execute(
|
||||
select(ActorProfile)
|
||||
.where(local_filter)
|
||||
.order_by(ActorProfile.preferred_username)
|
||||
.limit(remaining)
|
||||
.offset(local_offset)
|
||||
)
|
||||
).scalars().all()
|
||||
for lp in local_rows:
|
||||
results.append(RemoteActorDTO(
|
||||
id=0,
|
||||
actor_url=f"https://{domain}/users/{lp.preferred_username}",
|
||||
inbox_url=f"https://{domain}/users/{lp.preferred_username}/inbox",
|
||||
preferred_username=lp.preferred_username,
|
||||
domain=domain,
|
||||
display_name=lp.display_name,
|
||||
summary=lp.summary,
|
||||
icon_url=None,
|
||||
))
|
||||
|
||||
# Prepend WebFinger result (deduped)
|
||||
if webfinger_result:
|
||||
existing_urls = {r.actor_url for r in results}
|
||||
if webfinger_result.actor_url not in existing_urls:
|
||||
results.insert(0, webfinger_result)
|
||||
total += 1
|
||||
|
||||
return results, total
|
||||
|
||||
# -- Following (outbound) -------------------------------------------------
|
||||
|
||||
async def send_follow(
|
||||
|
||||
@@ -246,6 +246,9 @@ class StubFederationService:
|
||||
async def search_remote_actor(self, session, acct):
|
||||
return None
|
||||
|
||||
async def search_actors(self, session, query, page=1, limit=20):
|
||||
return [], 0
|
||||
|
||||
async def send_follow(self, session, local_username, remote_actor_url):
|
||||
raise RuntimeError("FederationService not available")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user