diff --git a/server.py b/server.py index 2e23eb7..97ca832 100644 --- a/server.py +++ b/server.py @@ -95,6 +95,25 @@ class RecordRunRequest(BaseModel): output_name: str +class PublishCacheRequest(BaseModel): + """Request to publish a cache item from L1.""" + content_hash: str + asset_name: str + asset_type: str = "image" + origin: dict # {type: "self"|"external", url?: str, note?: str} + description: Optional[str] = None + tags: list[str] = [] + metadata: dict = {} + + +class UpdateAssetRequest(BaseModel): + """Request to update an existing asset.""" + description: Optional[str] = None + tags: Optional[list[str]] = None + metadata: Optional[dict] = None + origin: Optional[dict] = None + + # ============ Storage ============ def load_registry() -> dict: @@ -889,6 +908,66 @@ async def get_asset(name: str): return registry["assets"][name] +@app.patch("/registry/{name}") +async def update_asset(name: str, req: UpdateAssetRequest, user: User = Depends(get_required_user)): + """Update an existing asset's metadata. Creates an Update activity.""" + registry = load_registry() + if name not in registry.get("assets", {}): + raise HTTPException(404, f"Asset not found: {name}") + + asset = registry["assets"][name] + + # Check ownership + if asset.get("owner") != user.username: + raise HTTPException(403, f"Not authorized to update asset owned by {asset.get('owner')}") + + # Update fields that were provided + if req.description is not None: + asset["description"] = req.description + if req.tags is not None: + asset["tags"] = req.tags + if req.metadata is not None: + asset["metadata"] = {**asset.get("metadata", {}), **req.metadata} + if req.origin is not None: + asset["origin"] = req.origin + + asset["updated_at"] = datetime.now(timezone.utc).isoformat() + + # Save registry + registry["assets"][name] = asset + save_registry(registry) + + # Create Update activity + activity = { + "activity_id": str(uuid.uuid4()), + "activity_type": "Update", + "actor_id": f"https://{DOMAIN}/users/{user.username}", + "object_data": { + "type": asset.get("asset_type", "Object").capitalize(), + "name": name, + "id": f"https://{DOMAIN}/objects/{asset['content_hash']}", + "contentHash": { + "algorithm": "sha3-256", + "value": asset["content_hash"] + }, + "attributedTo": f"https://{DOMAIN}/users/{user.username}", + "summary": req.description, + "tag": req.tags or asset.get("tags", []) + }, + "published": asset["updated_at"] + } + + # Sign activity + activity = sign_activity(activity) + + # Save activity + activities = load_activities() + activities.append(activity) + save_activities(activities) + + return {"asset": asset, "activity": activity} + + def _register_asset_impl(req: RegisterRequest, owner: str): """Internal implementation for registering an asset.""" registry = load_registry() @@ -989,6 +1068,99 @@ async def record_run(req: RecordRunRequest, user: User = Depends(get_required_us ), user.username) +@app.post("/registry/publish-cache") +async def publish_cache(req: PublishCacheRequest, user: User = Depends(get_required_user)): + """ + Publish a cache item from L1 with metadata. + + Requires origin to be set (self or external URL). + Creates a new asset and Create activity. + """ + # Validate origin + if not req.origin or "type" not in req.origin: + raise HTTPException(400, "Origin is required for publishing (type: 'self' or 'external')") + + origin_type = req.origin.get("type") + if origin_type not in ("self", "external"): + raise HTTPException(400, "Origin type must be 'self' or 'external'") + + if origin_type == "external" and not req.origin.get("url"): + raise HTTPException(400, "External origin requires a URL") + + # Check if asset name already exists + registry = load_registry() + if req.asset_name in registry.get("assets", {}): + raise HTTPException(400, f"Asset name already exists: {req.asset_name}") + + # Create asset + now = datetime.now(timezone.utc).isoformat() + asset = { + "name": req.asset_name, + "content_hash": req.content_hash, + "asset_type": req.asset_type, + "tags": req.tags, + "description": req.description, + "origin": req.origin, + "metadata": req.metadata, + "owner": user.username, + "created_at": now + } + + # Add to registry + if "assets" not in registry: + registry["assets"] = {} + registry["assets"][req.asset_name] = asset + save_registry(registry) + + # Create ownership activity with origin info + object_data = { + "type": req.asset_type.capitalize(), + "name": req.asset_name, + "id": f"https://{DOMAIN}/objects/{req.content_hash}", + "contentHash": { + "algorithm": "sha3-256", + "value": req.content_hash + }, + "attributedTo": f"https://{DOMAIN}/users/{user.username}", + "tag": req.tags + } + + if req.description: + object_data["summary"] = req.description + + # Include origin in ActivityPub object + if origin_type == "self": + object_data["generator"] = { + "type": "Application", + "name": "Art DAG", + "note": "Original content created by the author" + } + else: + object_data["source"] = { + "type": "Link", + "href": req.origin.get("url"), + "name": req.origin.get("note", "External source") + } + + activity = { + "activity_id": str(uuid.uuid4()), + "activity_type": "Create", + "actor_id": f"https://{DOMAIN}/users/{user.username}", + "object_data": object_data, + "published": now + } + + # Sign activity + activity = sign_activity(activity) + + # Save activity + activities = load_activities() + activities.append(activity) + save_activities(activities) + + return {"asset": asset, "activity": activity} + + # ============ Activities Endpoints ============ @app.get("/activities")