Minimal ActivityPub update endpoint
ActivityPub updates objects by posting an ActivityStreams2 Update
Activity to the authenticated user's outbox
(discovered from their profile).
I store my photo albums as ActivityStreams2 Collections
, so this morning I made a minimal AP-compliant update endpoint today that lets me add captions to them. This is just the server componant. It lets me do:
curl -vX POST -H "Content-Type: application/activity+json" -H "Authorization: secret-token" -d @as-update.json https://path/to/endpoint
where as-update.json
contains:
{
"@context": "http://www.w3.org/activitystreams#",
"type": "Update",
"name": "Amy captioned a photo.",
"actor": "https://rhiaro.co.uk/about#me",
"object": {
"id": "https://uri/of/photo.jpg",
"name": "A brand new caption."
}
}
So far this replaces the entire object with the object embedded in the Activity. Hopefully we'll have a syntax to indicate partial updates in AP soon.
The code (PHP) at https://path/to/endpoint
contains a few functions that are at the discretion of the server (how to verify the authenticated user can write, what to do with the activity once it gets it, and exactly how to perform the update):
function verify_token($token){
// Magic to verify token passed in Authorization header here.
// I check it against a protected file on the server that contains a randomly generated long string.
return $response;
}
function store_activity($activity){
// Arbitrary server logic to store the activity that was posted.
// I just dump the JSON in a file.
return true;
}
function make_update($activity){
// Arbitrary server logic to perform the update on the object.
// I parse the value of object in the activity to work out where its data is stored in the filesystem, then rewrite the appropriate JSON file.
return true;
}
And here's the overall flow:
// Authentication
$headers = apache_request_headers();
if(isset($headers['Authorization'])) {
$token = $headers['Authorization'];
$response = verify_token($token);
$me = @$response['me'];
$iss = @$response['issued_by'];
$client = @$response['client_id'];
$scope = @$response['scope'];
}else{
header("HTTP/1.1 403 Forbidden");
echo "403: No authorization header set.";
exit;
}
if(empty($response)){
// Something went wrong with verification
header("HTTP/1.1 401 Unauthorized");
echo "401: Access token could not be verified.";
exit;
}elseif(stripos($me, "rhiaro.co.uk") === false || $scope != "update"){
// The wrong person and scope was returned when the token was verified.
header("HTTP/1.1 403 Forbidden");
echo "403: Access token was not valid.";
exit;
}else{
// Verified, good to go..
if(empty($_POST)){
$post = file_get_contents('php://input');
}
if(isset($post) && !empty($post)){
// Store activity
$id = date("Y-m-d_h:i:s")."_".uniqid();
if(store_activity($post)){
// Perform the update
if(make_update($post)){
header("HTTP/1.1 201 Created");
echo "Resource updated";
}else{
header("HTTP/1.1 500 Internal Server Error");
echo "500: Could not make update (probably a permissions issue). ";
}
}else{
header("HTTP/1.1 500 Internal Server Error");
echo "500: Could not store activity log (probably a permissions issue).";
}
}else{
header("HTTP/1.1 400 Bad Request");
echo "400: Nothing posted";
}
}
?>
Obviously I've stripped out my implementation-specific stuff to make it easier to read. The actual code on my server is here: github/rhiaro/img/pub.php.
The next thing I need to do is make a client that...
- authenticates the user (I usually use IndieAuth, delegating the hardparts to https://indieauth.com),
- discovers the update endpoint from their profile (which to conform to AP right now would be via a JSON property
outbox
), - presents a UI that lets the user choose a collection to edit (could be discoverable from profile via
streams
property, but to start with I'll probably just offer a URL input), - posts the new values to the update endpoint.