204 lines
6.0 KiB
Python
204 lines
6.0 KiB
Python
# 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)
|