Does anyone know of (or want to write) a tutorial for querying JSON in postgres for people who know SPARQL..?
json (9 out of 9)
- Doesn't have proper authentication, just a field to enter in a key that the server uses to verify me.
- Doesn't do endpoint discovery, so I have to type that in as well.
- Only understands how to display ActivityStreams2 Collections, and only knows how to edit items within the Collections.
- Specifically only knows how to add
name
,published
andtags
to items in a Collection. - 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.
- Explicit
type
on the activity (whereasPost
orCreate
is implied in the micropub example). - Metadata (eg.
published
andactor
) are attached to the activity, not the object. id
is on the object as in AS, rather than outside it as in micropub, so what you're sending is just the object.- It's implicitly an update (no
type
) because an object with anid
was supplied. - The new
content
is submitted, which will replace what exists. - The
updated
date is explicit.
Minimal ActivityPub update client
Today I finished morph, a client for posting ActivityStreams2 Update activities to an endpoint. The server handles this update activity however it wants, but the obvious thing to do is take the object
of the activity and make the indicated changes.
So far it:
Will be expanding its object editing abilities soon.
Code on github. See also: Minimal ActivityPub update endpoint.
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...
+ http://www.w3.org/TR/json-ld/#interpreting-json-as-json-ld
Amy added http://www.w3.org/TR/json-ld/#interpreting-json-as-json-ld to https://rhiaro.co.uk/bookmarks/
Micropubbing with json
If micropub used JSON instead of (/as well as) form encoding, what might submissions look like? Aaaron Parecki brainstormed about this on our Social APIs brainstorming doc. I'm going to repeat his examples for create/update/delete here, compare them with equivalent AS2.0 json, then explore other post 'types'.
Note: I've left out @context
from all AS examples because we agreed when missing, this is implied to be "https://www.w3.org/ns/activitystreams".
Create a new post
{
"object": {
"content": "hello world",
"category": ["indieweb"],
"published": "2015-05-15T13:06:00+02:00",
"author": "https://rhiaro.co.uk/about#me"
}
}
No URL is given so it's implied that a new post is being created. Most of this is Aaron's example but I added the explicit published date and author, but these could also be added automatically by the micropub endpoint as the current time and the authenticated user who is sending the request.
ISSUE: actor
vs author
may never get agreed upon. I think actor
is too abstract and author
is too specific. I'm a fan of agent
to mean 'whatever caused this to happen' (be it human, group, process or bot) but I know that's not an easy sell either. I think one side is going to have to suck it up, and I don't know who this is going to be. AS also has attributedTo
which may be a reasonable compromise but seems a bit clunky.
Response per micropub spec: 201 Created
and the URL of the post in the Location
header.
An AS2.0 Activity for creating a new post would look like:
{
"@type": "Post",
"published": "2015-05-15T13:06:00+02:00",
"actor": "https://rhiaro.co.uk/about#me",
"object": {
"content": "hello world",
"category": ["indieweb"]
}
}
Response per ActivityPump spec: "This has the side effect that of the object that they are posting is being created." - either I missed something more specific in the spec or this remains to be clarified (I'm not criticising, AP is very much under-development!).
I prefer the first one. Key differences:
There has been a bit of discussion about whether things like audience targeting (to
) would be better off attached to the object rather than the activity; I'm inclined to think there's no metadata related to the creating of an object that can't/shouldn't be attached to the object itself, rendering the activity redundant. I keep asking people of examples that contradict this, but so far nobody has given me one.
Update a post
Aaron's micropub example:
{
"url": "http://example.com/post/1",
"object": {
"content": "hello moon"
}
}
ActivityStreams:
{
"@type": "Update",
"object": "http://example.org/post/1"
}
But the latter doesn't contain the updates made to the object... I might be missing something, but there are no specifics in the ActivityPump spec about this; presumably the user updates the object directly and the Update
activity is generated from that, which is a curiously object-centric happening in an activity-centric world. Neither contain the updated date; presumably for micropub the endpoint is expected to fill this gap automatically, and possibly in the AS example the activity is meant to have a published
property which could also automatically be filled in by the server. I'd like to be explicit about the updated date, though I don't know if this is a must or a should or a may. An alternative I like is:
{
"object": {
"@id": "http://example.org/post/1",
"updated": "2015-05-23T15:00:00+02:00",
"content": "hello moon"
}
}
ISSUE: I feel like persuading microformats afficondos to use @id
instead of url
might be a toughie.
Delete a post
Aaron's example:
{
"url": "http://example.com/post/1",
"action": "delete"
}
ActivityStreams:
{
"@type": "Delete",
"object": "http://example.org/post/1"
}
They look pleasantly similar :) ActivityPump specifies that "this must leave a shell of the object that will be displayed in activities which reference the deleted object. If the deleted object is requested the server should respond with the HTTP 410 Gone status code."
The empty 'shell' with the deleted date is useful if you want to preserve when you deleted things. Discussed this in #indiewebcamp IRC a bit, and tantek suggested that emptying a post of all metadata but the updated
property would suffice for implying a delete. An alternative to the examples above could be:
{
"object": {
"@id": "http://example.org/post/1",
"updated": "2015-05-23T15:00:00+:02:00",
"...": "...."
}
}
where "...": "..."
means... well, we need to define update behaviour before we can define delete-via-update behaviour. If omitting properties during an update removes them, then we don't need to send empty values for a delete. But if partial updates work, and the properties you send are changed and all others remain the same (which I think is better) then we need to send empty values for everything to delete them. This also allows the possibility of replacing content
with say "This post has been deleted" or "this post was removed due to its offensive nature" or "I spelt literally every word wrong and it seemed easier just to forget about it" and so on, which could be useful for smarter display of deleted posts (ie. as normal posts that have been updated to say they've been deleted). I'm sure someone will have something to say about the semantics of delete vs update; if so, please back it up with practical examples/use cases :)
We also need to consider that someone deleting a post may not want to leave a trace, even if just an updated
date and simply return a 410 if someone tries to access. This should be allowed, with the caveat that it's not possible to advise other servers that may have consumed the post that it's gone if they don't actively try to retrieve it and see the 410.
Liking a post
Or posting a like. Depending on whether you have object- or activity-tinted glasses. New grounds, so I'll start with what exists in ActivityStreams:
{
"@type": "Like",
"published": "2015-05-15T13:06:00+02:00",
"actor": "https://rhiaro.co.uk/about#me",
"object": "https://theperplexingpariah.co.uk/2015/05/a-post",
}
Remember in microformats we have the like-of
property which would have the same value as object
does above, this time, and the 'like' is a first-class post object in itself. So submitting one via micropub could look like:
{
"object": {
"published": "2015-05-15T13:06:00+02:00",
"author": "https://rhiaro.co.uk/about#me"
"like-of": "https://theperplexingpariah.co.uk/2015/05/a-post"
}
}
These are preeetttty different with regards to how terms are being used. A more AS-friendly version where the like is still treated as a first-class object would be:
{
"@type": "Like",
"published": "2015-05-15T13:06:00+02:00",
"actor": "https://rhiaro.co.uk/about#me",
"object": "https://theperplexingpariah.co.uk/2015/05/a-post",
"result": {
"published": "2015-05-15T13:06:00+02:00",
"author": "https://rhiaro.co.uk/about#me"
}
}
Even though a like post is implicitly created through the result
you have to go via the activity that generated it to find out what it's a like of so it doesn't really stand on its own. In the same way as audience targeting and other metadata is useful attached to the object I think having the like relation directly between the objects is practical. For semantic pedants, one post is a like-of
another post, so it's closer to having a like type than being a like verb. But it's an implicit type, rather than an explicit one. Just to reiterate: I'm not saying 'post A likes post B'. That would be silly. 'Post A like-of post B' pretty easily implies the author of post A likes post B. It's not worth trying to overthink that, and you don't need RDF inferencing to make sense of it.
In summary, even for things other than creating posts, I think just sending an object works. You can attach other things like "content": "I like Jessica's post"
too if you wanted, for nicer display.
Checkin
The closest example in ActivityStreams is an Arrive
activity:
{
"@context": "https://www.w3.org/ns/activitystreams",
"@type": "Arrive",
"actor": {
"@type": "Person",
"displayName": "Sally"
},
"location": {
"@type": "Place",
"displayName": "Work"
},
"origin": {
"@type": "Place",
"displayName": "Home"
}
}
I have never needed origin
with a checkin so I'm going to ignore that for now. I think I want to send to my micropub endpoint something more like:
{
"object": {
"published": "2015-05-23T13:00:00+02:00",
"location": "https://rhiaro.co.uk/home",
"content": "I got home"
}
}
or
{
"object": {
"published": "2015-05-23T13:00:00+02:00",
"location": "http://dbpedia.org/resource/Edinburgh",
"content": "Finally back in Auld Reekie!"
}
}
These are just like regular posts, but with a location property. Which is magically compatible with ActivityStremas and Microformats!
Where location
is always a URI, so that display name, latitude, longitude, etc, can be attached to that and don't need to be included in the checkin. I'm a big fan of not nesting. If you submit a checkin with a nested venue object, the endpoint has to try to create the venue before it can do the checkin, which means it needs to attempt to de-dup venues as part of the checkin process; and if the creation fails then there's more hassle... there's probably a smart way of designing the UI plus use of javascript that can make this pretty smooth, but it's definitely not something I want to have to deal with.
RSVP
In AS we have Accept
which is supposed to have an object
of Invite
which in turn has an object
of Event
(whether these are all nested in the Accept
activity or just referenced by URI doesn't really matter, the structure is the same. I included the nesting here cos I just copied the example directly from the AS vocabulary spec):
{
"@context": "https://www.w3.org/ns/activitystreams",
"@type": "Accept",
"actor": {
"@type": "Person",
"displayName": "Sally"
},
"object": {
"@type": "Invite",
"actor": "http://john.example.org",
"object": {
"@type": "Event",
"displayName": "A Party!"
}
}
}
While it's all semantically-pedantically very lovely, I reckon the microformats way of just having an RSVP be a regular post with a reply-to
another regular post which happens to have a location
, start
and end
(thus making it an event) is easier to follow and generally simpler (and effective as evidenced by all the people posting and RSVPing to indie events). In JSON, I think that'd look like:
{
"object": {
"published: "2015-05-23T13:00:00+02:00",
"reply-to": "http://indiewebcamp.com/2015/Edinburgh",
"rsvp": "Yes",
"content": "Really looking forward to IWC Edinburgh!"
}
}
Microformats does have invitations too though - what's the fun of RSVPing to an event if you can't invite your friends! Invite posts can be part of an event post, part of an RSVP post, or a post by themselves. That's because all we're really doing to create them is adding invitee
properties for everyone you want to invite, and then sending them webmentions. If your invitation is not also the original event, you can send the original event post a webmention too (since you've mentioned it, after all) which could allow it to update its own list of who has been invited. Your invitee should reply with their RSVP both to your invitation and the event (which they can do with one post). Brainstorming about this, which is by no means sent in stone, on IWC wiki.
Here is an event with a list of invitees:
{
"object": {
"published: "2015-05-23T13:00:00+02:00",
"name": "IndieWebCamp Edinburgh",
"start": "2015-07-25T10:00:00+01:00"
"end": "2015-07-26T18:00:00+01:00",
"location": "http://dbpedia.org/resource/Edinburgh",
"content": "Bring your friends to IWC Edinburgh!",
"invitee": ["http://tantek.com", "http://theperplexingpariah.co.uk", "http://aaronpk.com"]
}
}
Consuming
Neither ActivityStreams nor Micorformats have things to do with eating and drinking, but this is something I log on my site. My micropub endpoint currently recognises Aaron's p3k-food
property so I'll use that here, but I'd prefer it to be something more generic..
{
"object": {
"published": "2015-05-23T17:00:00+02:00",
"p3k-food": "A burrito",
"category": ["mexican","burrito","yum"]
}
}
A potentially useful fall back for something that wants to display this post but doesn't understand p3k-food
could just to be to treat it as content
.
Yes, I do desire to tag my food posts.
The value of p3k-food
could also be a handy URI for the food, if one existed, which when dereferenced could give you things like different language display names, calorie count, cuisine affiliations, ingredients, etc.
Conclusion
My micropub endpoint will accept JSON and expect an object
with a bunch of properties. From these properties my server can infer how to treat them with regards to who to send webmentions to (ie. who to explicitly notify that this object was created; if I was implementing ActivityPump I'd send the whole object to these people's inboxes) and how to display them. It dosen't need explicit types, or properties spread over activities and objects to do this.
In anther post I'll run through this for (un)follows, adding to and removing from collections, exercise posts, music listens, code commits, travel plans, offers and requests, and see if they hold up the same.
If you have examples of online social stuff you do that you don't think you could publish and propagate this way, please tell me all about it. I really want to know.
Amy's mini Alpine Adventure
I picked Innsbruck pretty randomly as a mid-point between Dusseldorf (where I was for IWC) and Florence (where I need to be for WWW). I haven't been to Austria before, or even the Alps bar a day-trip during a school trip to France like ten years ago. The first day I was here was clear, sunny and crazy hot, so I started loving Innsbruck pretty immediately. I wandered around town, got a feel for the place, and found the only place I could get vegan pizza and eiskaffee.
The Inn river is fast-flowing and a cartoonish blue-green. The houses are bright colours, and everywhere is walkable. The town is surrounded by a ring of snow-capped mountains.
I booked a hostel so I wouldn't have to socialise too much, as I need a break from people between all of these conferences. The hostel is super quiet, and mostly pretty good. The first night I shared my dorm with two Canadian girls. They told me the funicular up one of the mountains is reasonably priced, and that it was silly to think of walking to the base as it's really far, and I should get the bus. Also that I should pay the EUR 2.60 for a bus ticket, as one of them didn't and got caught and fined ;)
Yesterday I hacked a bunch on Slog'd in the morning, then at 2pm set off with vague intentions of either climbing a mountain or swimming in a lake, ideally both. I ignored the advice of the Canadian girls, and walked to Hungerburg where the cable car starts. It was less than an hour from the hostel, and the mountain parts were easy. Turns out the cable car to the top is over 25 EUR, and it's about a 3 hour climb to Seegrube. And climbing is WAY more interesting. However, from Seegrube to the summit at Halekefar I was advised there is too much snow for climbing, so the cable car for that stretch is mandatory (and Seegrube-Halekefar is EUR 6.10 return, which I can handle). So I figured I'd climb to Seegrube, take the cable car to the summit and back, and climb all the way back on foot. But by that point, the odds of me making it to Seegrube before the cablecar stopped running at 1730 were pretty slim. I toyed with trying to find an ATM and taking the cablecar all the way, but decided I'd prefer to climb anyway and can come back the next day, earlier.
Then I walked to Lake Rossau, which the internet claims cost EUR 3.50 to swim in but I mean, it's a lake, so nobody seemed to be enforcing this. So I swam in the lake surrounded by mountains and it was beautiful.
Then I came back to the hostel and added a first pass at ActivityStreams2.0 JSON to my site. Woo! Productive day.
Today it is raining heavily and the mountains are shrouded in clouds. Naturally. Turns out I'm going to give getting to Halekefar a shot anyway, even if I can't see anything from the top, thanks to encouragement from my new hostel roomie, a fairly old Vietnamese lady who lives in Belgium.
Also, I'll post some photos just as soon as I implement display of albums and attaching Collections to posts on Slog'd.... Aaaaany day now.
🔁 http://aaronparecki.com/notes/2015/05/14/4/
Amy shared http://aaronparecki.com/notes/2015/05/14/4/
🔁 https://twitter.com/aaronpk/status/599040815789252608
Amy shared https://twitter.com/aaronpk/status/599040815789252608
+ https://as-test-harness.mybluemix.net/#about
Amy added https://as-test-harness.mybluemix.net/#about to https://rhiaro.co.uk/bookmarks/