import os
import json
import base64
import hashlib
from datetime import datetime, timedelta
from typing import Optional, List, Dict, Any
import logging
from ..models import (
FeedGeneratorDescription, FeedSkeletonResponse, FeedSkeletonItem,
DidDocument, DidDocumentService
)
logger = logging.getLogger(__name__)
class FeedGeneratorService:
"""Service for Bluesky feed generator protocol."""
def __init__(self):
self.service_did = os.environ.get("FEED_GENERATOR_SERVICE_DID", "")
self.feed_key = os.environ.get("FEED_GENERATOR_FEED_KEY", "main-feed")
self.display_name = os.environ.get("FEED_GENERATOR_DISPLAY_NAME", "My Smart Feed")
self.description = os.environ.get("FEED_GENERATOR_DESCRIPTION",
"A personalized feed based on your liked posts")
self.service_url = os.environ.get("FEED_GENERATOR_SERVICE_URL", "")
def describe(self) -> FeedGeneratorDescription:
"""Get feed generator description."""
is_configured = bool(self.service_did and self.service_url)
feed_uri = ""
if is_configured:
# The DID in the feed URI is the owner DID, which we don't know until runtime
# This will be filled in by the frontend
feed_uri = f"at://<owner-did>/app.bsky.feed.generator/{self.feed_key}"
return FeedGeneratorDescription(
feed_uri=feed_uri,
feed_key=self.feed_key,
name=self.display_name,
description=self.description,
is_configured=is_configured,
service_did=self.service_did if is_configured else None
)
def describe_for_xrpc(self) -> Dict[str, Any]:
"""Get feed description in XRPC format."""
return {
"encoding": "application/json",
"body": {
"did": self.service_did,
"feeds": [
{
"uri": f"at://{self.service_did}/app.bsky.feed.generator/{self.feed_key}",
"cid": "bafyreihh6dqzwyoa74a6vulbf6waz7ft3dqbhy5w7fy6t27lf55n5tdigy" # Dummy CID
}
],
"links": {
"generator": self.service_url
}
}
}
def get_did_document(self) -> DidDocument:
"""Get DID document for service identity."""
if not self.service_did:
raise InvalidOperationException("Feed generator service not configured")
return DidDocument(
id=self.service_did,
service=[
DidDocumentService(
id="#bsky_fg",
type="BskyFeedGenerator",
service_endpoint=self.service_url
)
]
)
def get_skeleton(self, feed: str, viewer_did: Optional[str],
limit: Optional[int] = 50, cursor: Optional[str] = None,
store=None) -> FeedSkeletonResponse:
"""Get feed skeleton (list of post URIs)."""
if not self.service_did:
raise InvalidOperationException("Feed generator not configured")
# Parse feed URI to get owner DID
# at://<did>/app.bsky.feed.generator/<feed_key>
parts = feed.replace("at://", "").split("/")
if len(parts) < 3:
raise InvalidOperationException("Invalid feed URI")
owner_did = parts[0]
feed_key = parts[-1]
if feed_key != self.feed_key:
raise InvalidOperationException("Feed not found")
# Get state from store
if store is None:
return FeedSkeletonResponse()
# Default generator
from ..models import GeneratorDefinition
definition = GeneratorDefinition(
id="balanced",
name="Balanced",
description="Balanced ranking",
strategy="hybrid"
)
state = store.get_state(owner_did, "balanced", None, definition)
# Convert to skeleton items
items = []
start_idx = int(cursor) if cursor else 0
end_idx = start_idx + (limit or 50)
for item in state.items[start_idx:end_idx]:
items.append(FeedSkeletonItem(post=item.subject_uri))
next_cursor = str(end_idx) if end_idx < len(state.items) else None
return FeedSkeletonResponse(
feed=items,
cursor=next_cursor
)
class InvalidOperationException(Exception):
"""Exception for invalid operations."""
pass