Mastodon Poll in ActivityPub
Published on
I started running my own ActivityPub instance with Takahē.
I got interested in contributing to the project and one of my first implemented features was the poll support.
While implementing it I noticed that the informations about this topic were scattered throughout multiple places.
So, for me to remember how it works, and maybe to help someone looking for the same information I decided to write this article.
Poll or Question? How ActivityPub handles it?
The first thing we need to know is that ActivityPub does not support Poll as a native type in it’s protocol definition.
The closest type that we get is Question, that is used to represent any kind of question, including open-ended question and that is an important information to have in mind.
Looking in the ActivityPub documentation on how to represent questions we can see right the way how to represent one:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://polls.example.org/question/1",
"type": "Question",
"content": "What is the answer to life the universe and everything?"
}
The question above is an open-ended question, but you can have a single-choice question using the oneOf
property:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://polls.example.org/question/2",
"type": "Question",
"content": "What is the answer to life the universe and everything?",
"oneOf":[
{ "name":"123" },
{ "name":"42" },
{ "name":"666" }
]
}
You can also have a multiple-choice question using the anyOf
property:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://polls.example.org/question/3",
"type": "Question",
"content": "Which are your favorite seasons?",
"anyOf":[
{ "name":"Spring" },
{ "name":"Summer" },
{ "name":"Autumn" },
{ "name":"Winter" }
]
}
When you don’t want to receive any more answer you can indicate it by closing your question with the closed
property indicating the DateTime
of when it was closed:
{
"@context": "https://www.w3.org/ns/activitystreams",
"id": "http://polls.example.org/question/4",
"type": "Question",
"content": "Fast before it's too late, who wants a slice of cake?",
"closed": "2023-01-01T00:00:00Z"
}
Those are the base properties of an ActivityPub question. Further down this article we’ll be able to see what Mastodon adds to it.
Answering to a Question
The answer to a question in ActivityPub is a bit vague. It doesn’t have an specific type, but rather required properties:
{
"@context": "https://www.w3.org/ns/activitystreams",
"attributedTo": "http://bob.example.org",
"inReplyTo": "http://polls.example.org/question/2",
"name": "42"
}
The property attributedTo
indicates who is answering to the question, while the property inReplyTo
points to the URI of the question you are answering to.
Although it’s not mentioned in the documentation we can infer that the name
property is the answer to the question and in questions where we have choices to select, the answer text must match one of the given choices.
Now that we know what ActivityPub has to say about it, we need to take a look on how Mastodon decided to implement it.
The poll in Mastodon
Mastodon offers some endpoints to deal with polls:
GET /api/v1/polls/:id HTTP/1.1
to retrieve a poll (docs)POST /api/v1/polls/:id/votes HTTP/1.1
to vote on a poll (docs)POST /api/v1/statuses HTTP/1.1
to create a poll through the status API (docs)GET /api/v1/statuses/:id HTTP/1.1
to retrieve a poll through the status API (docs)DELETE /api/v1/statuses/:id HTTP/1.1
to delete a poll through the status API (docs)
At the moment I’m writing this article an entity that represents a poll in Mastodon looks like this:
{
"id": "12345",
"expires_at": "2023-01-01T23:04:45.000Z",
"expired": true,
"multiple": false,
"votes_count": 10,
"voters_count": null,
"options": [
{"title": "Option 1", "votes_count": 6},
{"title": "Option 2", "votes_count": 4},
],
"emojis": [],
"voted": false,
"own_votes": [],
}
The actual text asking the question to be answered by the poll is located at the content
field of the status the same way a common post should be.
You can check the poll entity documentation to know more about it.
Mastodon Poll in ActivityPub
Going back to ActivityPub, the Mastodon poll indeed uses type Question
provided by ActivityPub, but it also adds some other fields to be able to properly represent the object that we get in Mastodon API.
The toot
Mastodon extension is also used to add the votersCount
property.
{
"@context":[
"https://www.w3.org/ns/activitystreams",
{
"toot":"http://joinmastodon.org/ns#",
"votersCount":"toot:votersCount"
},
"https://w3id.org/security/v1"
],
"id":"https://polls.example.org/users/sally/statuses/1234",
"type":"Question",
"votersCount":10,
"attributedTo":"https://polls.example.org/users/sally",
"closed":"2023-01-01T20:04:45Z",
"content":"<p>What is your favorite starter pokemon?</p>",
"endTime":"2023-01-01T20:04:45Z",
"oneOf":[
{
"type":"Note",
"name":"Charmander",
"replies":{
"type":"Collection",
"totalItems":5
}
},
{
"type":"Note",
"name":"Bulbasaur",
"replies":{
"type":"Collection",
"totalItems":2
}
},
{
"type":"Note",
"name":"Squirtle",
"replies":{
"type":"Collection",
"totalItems":3
}
}
],
"published":"2023-01-01T01:00:00Z",
"url":"https://polls.example.org/@sally/1234"
}
The endTime
is used to add an expiration time counter to he poll while the closed
will be present only when the poll was closed. Both, when filled should have the same value.
The Mastodon extension of votersCount
was an addition that is useful in the multiple-choice case, where the sum of votes can be different of the number of participants in the poll.
It also made an adaptation to the choices in order to keep track of the number of votes per choice:
{
"type":"Note",
"name":"Charmander",
"replies":{
"type":"Collection",
"totalItems":5
}
}
The choices got transformed into objects of Note
type that inherit all properties of Object
so it was possible to use the replies
property to store the total of votes a choice got.
Mastodon vote in ActivityPub
To cast a vote in a poll, Mastodon uses the flow indicated in ActivityPub, but with a small difference:
{
"id":"https://answer.example.org/users/bob#votes/389574/activity",
"to":"https://polls.example.org/@sally@polls.example.org/",
"type":"Create",
"actor":"https://answer.example.org/users/bob",
"object":{
"id":"https://answer.example.org/users/bob#votes/389574",
"to":"https://polls.example.org/@sally@polls.example.org/",
"name":"Charmander",
"type":"Note",
"inReplyTo":"https://polls.example.org/@sally@polls.example.org/posts/1234/",
"attributedTo":"https://answer.example.org/users/bob"
},
"@context":[
"https://www.w3.org/ns/activitystreams",
{
"toot":"http://joinmastodon.org/ns#",
"votersCount":"toot:votersCount",
},
"https://w3id.org/security/v1"
]
}
The type of the created object is Note
to match the type of the options.
Looking to a discussion in ActivityPub forum about Mastodon implementation of a vote we have a summary about the object’s fields:
id: ...
type: Note
name: <the exact text of the poll option>
inreplyTo: <the status that holds the poll>
attributedTo: <you>
to: <the poll creator>
This confirms the that Mastodon decided to go with our inference about the name
property.
When dealing with multiple-choice polls, Mastodon didn’t changed anything in the object format, instead they just send more than one Create
message from the same sender targeting the same poll, but with an answer to another choice.
So at the end, to be sure that the message you got is an answer to a poll and not a regular post or reply you need to check some things:
- The target to the
inReplyTo
property must be aQuestion
- The type of the object is
Note
but with nocontent
property - The answer is located in the
name
property
Conclusion
Operations around the ActivityPub Question type are not super clear, and Mastodon docs also don’t fill all the blanks in it, but I hope with those info you can know a bit more about how things work behind the scenes 🙂
This work is licensed under a Creative Commons Attribuition-ShareAlike 4.0 International License .