# primitive/activitypub/ownership.py """ Ownership integration between ActivityPub and Registry. Connects actors, activities, and assets to establish provable ownership. """ import json from dataclasses import dataclass from pathlib import Path from typing import Any, Dict, List, Optional from .actor import Actor, ActorStore from .activity import Activity, CreateActivity, ActivityStore from .signatures import sign_activity, verify_activity_ownership from ..registry import Registry, Asset @dataclass class OwnershipRecord: """ A verified ownership record linking actor to asset. Attributes: actor_handle: The actor's fediverse handle asset_name: Name of the owned asset cid: SHA-3 hash of the asset activity_id: ID of the Create activity establishing ownership verified: Whether the signature has been verified """ actor_handle: str asset_name: str cid: str activity_id: str verified: bool = False class OwnershipManager: """ Manages ownership relationships between actors and assets. Integrates: - ActorStore: Identity management - Registry: Asset storage - ActivityStore: Ownership activities """ def __init__(self, base_dir: Path | str): self.base_dir = Path(base_dir) self.base_dir.mkdir(parents=True, exist_ok=True) # Initialize stores self.actors = ActorStore(self.base_dir / "actors") self.activities = ActivityStore(self.base_dir / "activities") self.registry = Registry(self.base_dir / "registry") def create_actor(self, username: str, display_name: str = None) -> Actor: """Create a new actor identity.""" return self.actors.create(username, display_name) def get_actor(self, username: str) -> Optional[Actor]: """Get an actor by username.""" return self.actors.get(username) def register_asset( self, actor: Actor, name: str, cid: str, url: str = None, local_path: Path | str = None, tags: List[str] = None, metadata: Dict[str, Any] = None, ) -> tuple[Asset, Activity]: """ Register an asset and establish ownership. Creates the asset in the registry and a signed Create activity proving the actor's ownership. Args: actor: The actor claiming ownership name: Name for the asset cid: SHA-3-256 hash of the content url: Public URL (canonical location) local_path: Optional local path tags: Optional tags metadata: Optional metadata Returns: Tuple of (Asset, signed CreateActivity) """ # Add to registry asset = self.registry.add( name=name, cid=cid, url=url, local_path=local_path, tags=tags, metadata=metadata, ) # Create ownership activity activity = CreateActivity.for_asset( actor=actor, asset_name=name, cid=asset.cid, asset_type=self._asset_type_to_ap(asset.asset_type), metadata=metadata, ) # Sign the activity signed_activity = sign_activity(activity, actor) # Store the activity self.activities.add(signed_activity) return asset, signed_activity def _asset_type_to_ap(self, asset_type: str) -> str: """Convert registry asset type to ActivityPub type.""" type_map = { "image": "Image", "video": "Video", "audio": "Audio", "unknown": "Document", } return type_map.get(asset_type, "Document") def get_owner(self, asset_name: str) -> Optional[Actor]: """ Get the owner of an asset. Finds the earliest Create activity for the asset and returns the actor if the signature is valid. """ asset = self.registry.get(asset_name) if not asset: return None # Find Create activities for this asset activities = self.activities.find_by_object_hash(asset.cid) create_activities = [a for a in activities if a.activity_type == "Create"] if not create_activities: return None # Get the earliest (first owner) earliest = min(create_activities, key=lambda a: a.published) # Extract username from actor_id # Format: https://artdag.rose-ash.com/users/{username} actor_id = earliest.actor_id if "/users/" in actor_id: username = actor_id.split("/users/")[-1] actor = self.actors.get(username) if actor and verify_activity_ownership(earliest, actor): return actor return None def verify_ownership(self, asset_name: str, actor: Actor) -> bool: """ Verify that an actor owns an asset. Checks for a valid signed Create activity linking the actor to the asset. """ asset = self.registry.get(asset_name) if not asset: return False activities = self.activities.find_by_object_hash(asset.cid) for activity in activities: if activity.activity_type == "Create" and activity.actor_id == actor.id: if verify_activity_ownership(activity, actor): return True return False def list_owned_assets(self, actor: Actor) -> List[Asset]: """List all assets owned by an actor.""" activities = self.activities.find_by_actor(actor.id) owned = [] for activity in activities: if activity.activity_type == "Create": # Find asset by hash obj_hash = activity.object_data.get("contentHash", {}) if isinstance(obj_hash, dict): hash_value = obj_hash.get("value") else: hash_value = obj_hash if hash_value: asset = self.registry.find_by_hash(hash_value) if asset: owned.append(asset) return owned def get_ownership_records(self) -> List[OwnershipRecord]: """Get all ownership records.""" records = [] for activity in self.activities.list(): if activity.activity_type != "Create": continue # Extract info actor_id = activity.actor_id username = actor_id.split("/users/")[-1] if "/users/" in actor_id else "unknown" actor = self.actors.get(username) obj_hash = activity.object_data.get("contentHash", {}) hash_value = obj_hash.get("value") if isinstance(obj_hash, dict) else obj_hash records.append(OwnershipRecord( actor_handle=actor.handle if actor else f"@{username}@unknown", asset_name=activity.object_data.get("name", "unknown"), cid=hash_value or "unknown", activity_id=activity.activity_id, verified=verify_activity_ownership(activity, actor) if actor else False, )) return records