GraphQL API¶
Mobilizon features a GraphQL API which allows to pragmatically interact with the application: search events, create events and virtually any operation that is possible in the WebUI. Authentication is handled with Json Web Tokens. Each instance provides a GraphQL playground at /graphiql
, such as https://mobilizon.fr/graphiql that you can use to explore the schema. You can also use the schema's representation with a tool like graphdoc
.
The API is accessible on the /api
route of a Mobilizon instance. Be careful that the API can be different from one version of Mobilizon to another. Test you code before upgrading.
Authentication¶
Some operations or accessing some contents require authentication and appropriate authorization (like group membership for example). You will need to make these calls using a
Follow the API auth section to know how to create an application and authenticate.
Users and profiles¶
The access token is linked to a user (an email). But in Mobilizon, the user is never directly exposed. A user can have one or many profiles (one identity for each kind of activities she/he is involved for example).
Profiles and groups are both "Actors" in Mobilizon.
When interacting with the API, it my be required for some operations to provide an "ActorID".
query:
query Identities {
identities {
...ActorFragment
}
}
fragment ActorFragment on Actor {
id
type
preferredUsername
name
url
}
There is no variables to provide as it queries the identities (profiles) of the owner of the access token.
response:
The response is the list of all your profiles
{
data: {
identities: [
[0]: {id: "123", name: "Profile 1", preferredUsername: "profile_1", type: "PERSON", url: "https://keskonfai.fr/@profile_1"},
[1]: {id: "456", name: "Profile 2", preferredUsername: "profile_2", type: "PERSON", url: "https://keskonfai.fr/@profile_2"},
[2]: {id: "789", name: "Profile 3", preferredUsername: "profile_3", type: "PERSON", url: "https://keskonfai.fr/@profile_3"},
[3]: {id: "945", name: "Profile 4", preferredUsername: "profile_4", type: "PERSON", url: "https://keskonfai.fr/@profile_4"}
]
}
}
Get my groups¶
It is recommanded to create events under a group instead of under a person, in order to favor communities of interest.
For that it is necessecary to be able to get identifiers of ones own groups.
Query¶
query LoggedUserMemberships($membershipName: String, $page: Int, $limit: Int) {
loggedUser {
id
memberships(name: $membershipName, page: $page, limit: $limit) {
total
elements {
role
actor {
...ActorFragment
}
parent {
...ActorFragment
}
}
}
}
}
fragment ActorFragment on Actor {
id
type
preferredUsername
name
}
Variables¶
Only variables for pagination are possible here as the user id is taken from the authentification token. If user have a lot of groups, it can be useful to query them all at once by changing the default page size (10), ie { "limit": 20 }
.
Response¶
Response is a list of members, consisting in an association of profiles, groups and roles.
For readability, the JSON response can be formated to a table using jq -r '.data.loggedUser.memberships.elements[]|[.actor.id, .actor.name, .parent.id, .parent.name, .role]|@tsv' | column -t -s $'\t' -N 'profile_id,profile,group_id,group,role'
.
This outputs a table as following:
profile_id profile group_id group role
5 profile_1 65835 group_A ADMINISTRATOR
5 profile_1 147460 group_B MEMBER
68 profile_2 1140592 group_C ADMINISTRATOR
68 profile_2 1143591 group_D ADMINISTRATOR
68 profile_2 356464 group_E MEMBER
122 profile_3 899048 group_F MEMBER
109687 profile_4 1125368 group_G ADMINISTRATOR
292071 profile_5 292123 group_H MODERATOR
A row must be read as : current user, under profile_1 is administrator of group_A. Also under profile_3, is member of group_F, etc.
When creating an event for a group:
- variable
organizerActorId
must be set toprofile_id
- variable
attributedToId
must be set togroup_id
.
Create an event¶
This operation requires an authentication and an actor id.
Mutation¶
mutation createEvent(
$organizerActorId: ID!
$title: String!
$attributedToId: ID
$description: String!
$beginsOn: DateTime!
$endsOn: DateTime
$status: EventStatus
$visibility: EventVisibility
$joinOptions: EventJoinOptions
$draft: Boolean
$tags: [String]
$picture: MediaInput
$onlineAddress: String
$phoneAddress: String
$category: String
$physicalAddress: AddressInput
$options: EventOptionsInput
$contacts: [Contact]
) {
createEvent(
organizerActorId: $organizerActorId
attributedToId: $attributedToId
title: $title
description: $description
beginsOn: $beginsOn
endsOn: $endsOn
status: $status
visibility: $visibility
joinOptions: $joinOptions
draft: $draft
tags: $tags
picture: $picture
onlineAddress: $onlineAddress
phoneAddress: $phoneAddress
category: $category
physicalAddress: $physicalAddress
options: $options
contacts: $contacts
) {
id
uuid
}
}
Variables¶
Only title and start date are mandatory but all fields are useful. Here is an example :
{
"organizerActorId": 68,
"attributedToId": 356464,
"title": "A l'école des explorateurs - Visite guidée Uzès, Ville d'art et d'histoire",
"beginsOn": "2020-10-29T00:00:00+01:00",
"endsOn": "2022-03-31T23:59:59+02:00",
"visibility": "PUBLIC",
"description": "<p>Accessible en fauteuil roulant en autonomie.</p>",
"draft": false,
"joinOptions": "FREE",
"local": true,
"onlineAddress": "https://www.tourismegard.com/",
"options": {
"commentModeration": "CLOSED",
"showStartTime": false,
"showEndTime": false
},
"phoneAddress": null,
"status": "CONFIRMED",
"tags": ["culturel"],
"physicalAddress": {
"description": ["Chapelle des Capucins"],
"geom": "4.419471;44.013957",
"locality": "Uzès",
"postalCode": "30700",
"street": "16 place Albert 1er",
"country": "France"
},
"picture": {
"media_id": "180"
}
}
Create an event for a group¶
When variable attributedToId
is not null, the event will be attributed to the group with the provided id. The provided actor must have the right to publish events for this group (having role member, moderator or administrator) for the mutation to be accepted.
Response¶
We could ask for any parts of the event, but here we asked to return id
and uuid
only as they are not known in advance and can be useful.
id
is the internal identifier of the event in the platform. It is required to update or delete the event.
uuid
is the public identifier of the event that is present in the URL when accessing the event page directly (eg: https://mobilizon.fr/events/e2464054-60f3-47d8-b6aa-68c810107068
)
It may be important to store these information for further usage in your business logic and probably to link them to the identifier of the same event in your own information system.
Update an event¶
Mutation¶
Updating an event is very similar to create it. The payload is the same, with the parts that are to be updated and the parts that are not changing.
mutation updateEvent($id: ID!, ...) {
updateEvent(eventId: $id, ...) {
id
uuid
}
}
Variables¶
In addition to what is required for a creation, an update require the id of the event.
Response¶
Same as for creation.
Delete an event¶
Mutation¶
mutation DeleteEvent($eventId: ID!) {
deleteEvent(eventId: $eventId) {
id
}
}
Variables¶
Only the id of the event is needed to perform a deletion.
Response¶
We don't need anything, but we can check the id to make sure it matches the one given as variable.
Search events¶
This operation does not require an authentication. However, without authentication, only public content is accessible.
Query¶
query SearchEvents(
$location: String
$radius: Float
$tags: String
$term: String
$type: EventType
$beginsOn: DateTime
$endsOn: DateTime
$eventPage: Int
$limit: Int
) {
searchEvents(
location: $location
radius: $radius
tags: $tags
term: $term
type: $type
beginsOn: $beginsOn
endsOn: $endsOn
limit: $limit
) {
total
elements {
id
title
uuid
beginsOn
picture {
id
url
name
__typename
}
status
tags {
...TagFragment
__typename
}
physicalAddress {
...AdressFragment
__typename
}
organizerActor {
...ActorFragment
__typename
}
attributedTo {
...ActorFragment
__typename
}
options {
...EventOptions
__typename
}
__typename
}
__typename
}
}
fragment EventOptions on EventOptions {
maximumAttendeeCapacity
remainingAttendeeCapacity
showRemainingAttendeeCapacity
anonymousParticipation
showStartTime
showEndTime
timezone
offers {
price
priceCurrency
url
__typename
}
participationConditions {
title
content
url
__typename
}
attendees
program
commentModeration
showParticipationPrice
hideOrganizerWhenGroupEvent
isOnline
__typename
}
fragment TagFragment on Tag {
id
slug
title
__typename
}
fragment AdressFragment on Address {
id
description
geom
street
locality
postalCode
region
country
type
url
originId
timezone
__typename
}
fragment ActorFragment on Actor {
id
avatar {
id
url
__typename
}
type
preferredUsername
name
domain
summary
url
__typename
}
The query is big because there are many parts in an event object : picture, location, organizer,group, tags, participants, in addition to the basic fields, title, dates, etc.
It is not mandatory to request all parts. It really depends on what information are needed in the response processing phase.
Variables¶
{
"beginsOn": "2022-02-28T23:00:00.000Z",
"endsOn": "2022-03-31T21:59:59.999Z",
"eventPage": 2,
"limit": 12,
"radius": 100,
"term": "culture",
"location": "u05cyfxcr"
}
It is possible to search by :
- time window, ie between two dates, using
beginsOn
andendsOn
, both are optional - distance from a location (
location
in geohash, radius inkm
) - term, in title or description (title has more weight than description for ranking)
- or a combination of any of them
By tags¶
{
"eventPage": 3,
"limit": 12,
"radius": null,
"tags": "gratuit"
}
Notice : if a list of tags (comma separated) is provided in the request, they will be used as "or" query not "and".
Pagination¶
Pagination is achieved using eventPage
(page number) and limit
(page size) variables.
Their is a hard limit on the number of elements sent in the response and a default limit too, usually 10.
Processing response¶
response is composed of:
- the total of elements corresponding to the query
- the elements of the requested page
▽ data: {searchEvents: {…}}
▽ searchEvents: {__typename: "Events", elements: […], total: 221}
__typename: "Events"
▽ elements: [{…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}]
▷ [0]: {__typename: "Event", beginsOn: "2021-12-31T23:00:00Z", id: "690367", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
▷ [1]: {__typename: "Event", beginsOn: "2022-01-01T05:00:00Z", id: "737870", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
▷ [2]: {__typename: "Event", beginsOn: "2022-01-31T23:00:00Z", id: "717177", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
▷ [3]: {__typename: "Event", beginsOn: "2022-01-31T23:00:00Z", id: "739045", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
▷ [4]: {__typename: "Event", beginsOn: "2022-02-24T23:00:00Z", id: "755134", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
▷ [5]: {__typename: "Event", beginsOn: "2022-02-28T19:00:00Z", id: "759028", options: {…}, organizerActor: null, physicalAddress: {…}, picture: null, stat…: …, …}
▷ [6]: {__typename: "Event", beginsOn: "2022-03-01T07:00:00Z", id: "714563", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
▷ [7]: {__typename: "Event", beginsOn: "2022-03-01T11:15:00Z", id: "759637", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
▷ [8]: {__typename: "Event", beginsOn: "2022-03-01T11:30:00Z", id: "759552", options: {…}, organizerActor: null, physicalAddress: {…}, picture: null, stat…: …, …}
▷ [9]: {__typename: "Event", beginsOn: "2022-03-01T13:00:00Z", id: "771678", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, status: …, …}
▷ [10]: {__typename: "Event", beginsOn: "2022-03-01T18:40:00Z", id: "762675", options: {…}, organizerActor: null, physicalAddress: {…}, picture: null, sta…: …, …}
▷ [11]: {__typename: "Event", beginsOn: "2022-03-02T07:00:00Z", id: "714023", options: {…}, organizerActor: null, physicalAddress: {…}, picture: {…}, stat…: …, …}
total: 221
They can be used to display a list, thumbnails in a grid, a map, etc.
Media upload¶
Mutations that include a file upload use a special encoding.
The request body is encoded with the Content-Type multipart/form-data
, with (at least) three named parts:
query
: The raw graphql query (not a JSON encoded object).variables
: A JSON-encoded variables object. The values for fields of typeUpload
do not contain the file, but an arbitrary ID, which references the form-data field where the uploaded file is contained.<id>
: The file content prefixed withContent-Type: application/octet-stream
header.
Standardization Efforts
There is a "spec" for this general problem, though this is not what mobilizon implements.
Quirks
The multipart delimiter must follow the format ---------------------------164507724316293925132493775707
This means that the delimiters generated by default by golang's mime/multipart
package are not accepted.
Clients¶
Even though a request handler for this is quickly written in most languages, clients known to follow this request structure out of the box are:
Example request body¶
-----------------------------164507724316293925132493775707
Content-Disposition: form-data; name="query"
mutation uploadMedia($file: Upload!, $name: String!) {
uploadMedia(file: $file, name: $name) { id }
}
-----------------------------164507724316293925132493775707
Content-Disposition: form-data; name="variables"
{"name":"foo.jpg","file":"image1"}
-----------------------------164507724316293925132493775707
Content-Disposition: form-data; name="image1"; filename="foo.jpg"
Content-Type: image/jpeg
����JFIFHH��&vExifII*
�
�▒�(
[...]
-----------------------------164507724316293925132493775707
Debugging and development¶
Inspect web application in a web browser¶
When struggling with the API, keep in mind that the Web application is using it. So you can still spy on it. In a web browser, use developer tools (F12 in Firefox) to look at the requests that are sent to the API by the Webapp when performing an action, and the response that is sent back by the server. For the request, it will be GraphQL query format, with variables as JSON. For the response, it will be JSON format.
Low-level tools¶
Before moving to a high level programming language and facing bugs, it is a good idea to test API calls with a lower level tools, such as :
- Curl a command line HTTP client included in any Linux distribution and other systems
- HTTPie an other command line HTTP client
- podman, not open source but specialized in API testing
- graphiql playground already mentioned
- you name it
Programming language integrations¶
- for Python, use gql library, an example here
- for bash, use
curl
, an example here
Support¶
You can seek for support on the forum or the chat.
Have fun !