# primitive/activitypub/activity.py """ ActivityPub Activity types. Activities represent actions taken by actors on objects. Key activity types for Art DAG: - Create: Actor creates/claims ownership of an object - Announce: Actor shares/boosts an object - Like: Actor endorses an object """ import json import time import uuid from dataclasses import dataclass, field from pathlib import Path from typing import Any, Dict, List, Optional from .actor import Actor, DOMAIN def _generate_id() -> str: """Generate unique activity ID.""" return str(uuid.uuid4()) @dataclass class Activity: """ Base ActivityPub Activity. Attributes: activity_id: Unique identifier activity_type: Type (Create, Announce, Like, etc.) actor_id: ID of the actor performing the activity object_data: The object of the activity published: ISO timestamp signature: Cryptographic signature (added after signing) """ activity_id: str activity_type: str actor_id: str object_data: Dict[str, Any] published: str = field(default_factory=lambda: time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())) signature: Optional[Dict[str, Any]] = None def to_activitypub(self) -> Dict[str, Any]: """Return ActivityPub JSON-LD representation.""" activity = { "@context": "https://www.w3.org/ns/activitystreams", "type": self.activity_type, "id": f"https://{DOMAIN}/activities/{self.activity_id}", "actor": self.actor_id, "object": self.object_data, "published": self.published, } if self.signature: activity["signature"] = self.signature return activity def to_dict(self) -> Dict[str, Any]: """Serialize for storage.""" return { "activity_id": self.activity_id, "activity_type": self.activity_type, "actor_id": self.actor_id, "object_data": self.object_data, "published": self.published, "signature": self.signature, } @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Activity": """Deserialize from storage.""" return cls( activity_id=data["activity_id"], activity_type=data["activity_type"], actor_id=data["actor_id"], object_data=data["object_data"], published=data.get("published", ""), signature=data.get("signature"), ) @dataclass class CreateActivity(Activity): """ Create activity - establishes ownership of an object. Used when an actor creates or claims an asset. """ activity_type: str = field(default="Create", init=False) @classmethod def for_asset( cls, actor: Actor, asset_name: str, cid: str, asset_type: str = "Image", metadata: Dict[str, Any] = None, ) -> "CreateActivity": """ Create a Create activity for an asset. Args: actor: The actor claiming ownership asset_name: Name of the asset cid: SHA-3 hash of the asset content asset_type: ActivityPub object type (Image, Video, Audio, etc.) metadata: Additional metadata Returns: CreateActivity establishing ownership """ object_data = { "type": asset_type, "name": asset_name, "id": f"https://{DOMAIN}/objects/{cid}", "contentHash": { "algorithm": "sha3-256", "value": cid, }, "attributedTo": actor.id, } if metadata: object_data["metadata"] = metadata return cls( activity_id=_generate_id(), actor_id=actor.id, object_data=object_data, ) class ActivityStore: """ Persistent storage for activities. Activities are stored as an append-only log for auditability. """ def __init__(self, store_dir: Path | str): self.store_dir = Path(store_dir) self.store_dir.mkdir(parents=True, exist_ok=True) self._activities: List[Activity] = [] self._load() def _log_path(self) -> Path: return self.store_dir / "activities.json" def _load(self): """Load activities from disk.""" log_path = self._log_path() if log_path.exists(): with open(log_path) as f: data = json.load(f) self._activities = [ Activity.from_dict(a) for a in data.get("activities", []) ] def _save(self): """Save activities to disk.""" data = { "version": "1.0", "activities": [a.to_dict() for a in self._activities], } with open(self._log_path(), "w") as f: json.dump(data, f, indent=2) def add(self, activity: Activity) -> None: """Add an activity to the log.""" self._activities.append(activity) self._save() def get(self, activity_id: str) -> Optional[Activity]: """Get an activity by ID.""" for a in self._activities: if a.activity_id == activity_id: return a return None def list(self) -> List[Activity]: """List all activities.""" return list(self._activities) def find_by_actor(self, actor_id: str) -> List[Activity]: """Find activities by actor.""" return [a for a in self._activities if a.actor_id == actor_id] def find_by_object_hash(self, cid: str) -> List[Activity]: """Find activities referencing an object by hash.""" results = [] for a in self._activities: obj_hash = a.object_data.get("contentHash", {}) if isinstance(obj_hash, dict) and obj_hash.get("value") == cid: results.append(a) elif a.object_data.get("contentHash") == cid: results.append(a) return results def __len__(self) -> int: return len(self._activities)