227 lines
7.1 KiB
Python
227 lines
7.1 KiB
Python
# 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
|