CLEMA protocol

Presence (Status)

A user's presence is four things: whether they can be reached (online), whether somebody is looking at a client of theirs (active), what they say about being reachable (state), and a custom status of their own — an emoji with a caption.

Get Contact Statuses

Request (all contacts):

{"id": "40", "type": "presence.get"}

Request (specific user):

{
  "id": "40",
  "type": "presence.get",
  "data": {
    "user_id": 67890
  }
}

Success:

{
  "id": "40",
  "type": "presence.get",
  "ok": true,
  "data": {
    "statuses": [
      {"user_id": 67890, "online": true, "active": true, "state": "away", "custom": {"emoji": "🍕", "text": "Lunch", "until": "2024-01-15T15:00:00Z"}, "time": "2024-01-15T14:00:00Z"},
      {"user_id": 67891, "online": true, "active": false, "last_active_at": "2024-01-15T12:30:00Z", "state": "available", "time": "2024-01-15T09:00:00Z"},
      {"user_id": 67892, "online": false, "active": false, "last_active_at": "2024-01-15T10:00:00Z", "custom": {"emoji": "🏖️", "text": "On vacation"}, "time": "2024-01-15T10:00:00Z"}
    ]
  }
}
Field Type Description
user_id number The user's ID
online boolean Whether the user can be seen online
active boolean Whether somebody is looking at a client of the user
last_active_at string While nobody is, when somebody last was (RFC 3339); absent when that was never known
state string What the user says about being reachable; only while online
custom object The user's custom status; absent when there is none
time string When the user last went online or offline (RFC 3339)

State field: what the user says about being reachable — available, away, busy or invisible. It is kept with the account until the user changes it: going offline and coming back does not reset it, so a phone that falls asleep does not turn busy into available. It is reported only while the user is online, because an account that is gone is not away. A new account starts available.

invisible keeps the user online for themselves and offline for everybody else: contacts receive presence.offline when the user turns invisible, see them offline with the moment they did, and receive presence.online when they come back. Messages, pushes and everything else work as usual, except what would give the user away: a delivery they report is not recorded, and how far they read a direct conversation reaches the other side only when they are visible again, after presence.online — which tells it the messages read were delivered as well. Only the user's own devices are ever told invisible.

Custom field: an emoji with an optional caption that the user shows next to their name, such as 🍕 "Lunch" or 🏖️ "On vacation". Unlike the state it is shown whether the user is online or not. until, when present, is the moment it clears itself; once it has passed, it is no longer shown, and within a minute the server clears it and tells the user's devices and contacts.

Field Type Description
emoji string One emoji
text string Caption; absent when there is none
until string When the status clears itself (RFC 3339); absent to keep it until changed

A user is online while they can be reached: at least one of their connections is open, active or not, or at least one of their sessions is registered for pushes — unless they are invisible. time is when that last changed.

A user is active while somebody is looking at a client of theirs: one of their connections is active and they are not invisible. A page left in a background tab, or a phone a push keeps online, is online without being active, which is what a messenger shows as "last seen": last_active_at. Becoming active reaches contacts at once; nobody looking any more only after a grace of about half a minute without anybody looking again, so that a switch between apps tells them nothing, and then as presence.updated with the moment it began. A user who goes offline or invisible stops being active with it, and while invisible their last_active_at stays where it was when they became so. A transport adapter whose protocol has no word for it, OSCAR's, leaves active out.

Results include only users allowed by their current presence setting, which governs the custom status as much as the rest. A forbidden specific lookup returns user_not_found; an all-contacts lookup omits forbidden users. This avoids revealing whether a hidden user is online.

Asking about oneself is how a device learns what the account says: the answer carries the state whether or not the account is online, invisible included, and online says whether the account can be reached at all.

Set Presence

{"id":"41","type":"presence.set","data":{"state":"away"}}
{"id":"42","type":"presence.set","data":{"custom":{"emoji":"🍕","text":"Lunch","until":"2024-01-15T15:00:00Z"}}}
{"id":"43","type":"presence.set","data":{"custom":null}}
Field Type Required Description
state string No available, away, busy or invisible
custom object No A new custom status, or null to clear it

A request carries either field or both; a field left out stays as it is. The state and the custom status are independent: one may be away with a custom status of 🍕 "Lunch", or available with none.

Both apply to the whole account rather than to one device, so a client that sets them changes what every other one shows. They are kept until changed, whatever connections come and go; the state shows while the user is online, a custom status at once, online or not. The response is the caller's own status as asking about oneself gives it, and setting what is already set changes nothing and tells nobody. The account's other devices receive the new status as presence.self.

Presence Events (Push)

Server notifies when contacts come online, go offline, or change what they say about themselves. Every event carries the same object presence.get lists, as the receiver may see it.

User Online:

{
  "type": "presence.online",
  "data": {
    "user_id": 67890,
    "online": true,
    "active": true,
    "state": "available",
    "time": "2024-01-15T14:00:00Z"
  }
}

User Offline:

{
  "type": "presence.offline",
  "data": {
    "user_id": 67890,
    "online": false,
    "active": false,
    "last_active_at": "2024-01-15T14:30:00Z",
    "custom": {"emoji": "🏖️", "text": "On vacation"},
    "time": "2024-01-15T14:30:00Z"
  }
}

Presence Changed:

{
  "type": "presence.updated",
  "data": {
    "user_id": 67890,
    "online": true,
    "active": false,
    "last_active_at": "2024-01-15T14:09:30Z",
    "state": "away",
    "custom": {"emoji": "🍕", "text": "Lunch"},
    "time": "2024-01-15T14:10:00Z"
  }
}

presence.updated reports a new state of somebody online, somebody online becoming active or no longer, and a new custom status of anybody, online or not — set, cleared, or run out. A user turning invisible or visible again reaches contacts as presence.offline or presence.online.

These events are about contacts: they reach whoever lists the user, the user included if they listed themselves, with what contacts may see. What the account says about itself reaches its own devices separately:

Own Status Changed:

{
  "type": "presence.self",
  "data": {
    "user_id": 12345,
    "online": true,
    "active": true,
    "state": "invisible",
    "custom": {"emoji": "🍕", "text": "Lunch"},
    "time": "2024-01-15T14:10:00Z"
  }
}

presence.self carries the account's own status as asking about oneself gives it. It reaches the account's other devices when one of them sets the state or the custom status, and all of them when the custom status runs out.

A presence.offline sent because the receiver is no longer allowed to see the user carries nothing but user_id, online and an empty time.

Contact Requests

Somebody who writes to a user who does not have them as a contact — a direct message, a forward or a direct transfer — asks to be taken in. The user is told at once, before the message itself, by an event of their account stream:

{
  "type": "contact.requested",
  "seq": "45",
  "data": {
    "contact": {
      "user_id": 67890,
      "nick": "Alice",
      "avatar_hash": "a1b2c3d4",
      "requested_at": "2024-01-15T14:00:00Z"
    }
  }
}

The entry is the one contact events carry: nick is the name the person goes by (Add Contact), a snapshot, and requested_at when they asked.

A request is not a contact. No privacy setting that trusts contacts trusts it, and contact.list returns it apart from the contacts, in requests, newest first. The user takes the person in with contact.add, which makes them a contact and answers the request, or turns them away with contact.delete, which counts as taking them out of the list: nothing asks about them again, and no address book brings them back. A client answering a request it shows passes "request": true, so that an answer given after the request was settled elsewhere changes nothing. Either reaches the user's other clients as contact.added or contact.deleted. An address book that finds the person takes them in too, which answers the request: the uploading session learns it from matches[].added, and every session of the user from contact.added. Replying to a request does not answer it.

Nobody is asked about who is a contact already, somebody turned away or taken out of the list, or somebody a block separates from the user. Somebody who answers a direct chat the user started — whose first message is the user's — is not asked about either: the user chose to write to them, so they are taken in as a contact, and the user's clients are told with contact.added before the message:

{
  "type": "contact.added",
  "seq": "46",
  "data": {
    "contact": {"user_id": 67890, "nick": "Alice", "avatar_hash": "a1b2c3d4"},
    "presence": {"user_id": 67890, "online": true, "state": "available", "time": "2024-01-15T14:00:00Z"}
  }
}

A request's typing reaches the user as a contact's does. A block ends the requests between the two either way.

A bot is written to as a service is used: it asks nobody to be taken in, nobody asks it, and writing to one takes it into no contact list. Its typing reaches whoever it talks with all the same: it is in no direct chat but one they began.