Contact Management
A contact list belongs to one person and is account state: every change to it
is an event of the owner's account stream, contact.added,
contact.renamed, contact.deleted or contact.requested. The device that
makes a change is answered with the entry as it now stands, at the place of
its event; the owner's other devices, and a device that was away, receive the
event. Contact changes are operations: each carries an
operation_id, and a retry answers as the first request did. A change that
changes nothing, such as a rename to the name the entry has, makes no event and
its answer carries no seq.
Every command and event about an entry carries the same object:
| Field | Type | Description |
|---|---|---|
user_id |
number | The person's numeric ID |
nick |
string | The entry's name: the owner's for the person, or a snapshot of the name the person went by when nobody named them |
avatar_hash |
string | The person's avatar hash, absent without an avatar and across a block either way |
requested_at |
string | When a request was made, for a request |
phone |
string | The number in the owner's address book that found the person, on the change an address book made |
removed |
boolean | true for an entry that is gone from the list |
List Contacts
Request:
{"id": "30", "type": "contact.list"}
Success:
{
"id": "30",
"type": "contact.list",
"ok": true,
"data": {
"contacts": [
{"user_id": 67890, "nick": "Friend", "avatar_hash": "a1b2c3d4"},
{"user_id": 67891, "nick": "Work Buddy"}
],
"requests": [
{"user_id": 67892, "nick": "Stranger", "requested_at": "2024-01-15T14:00:00Z"}
]
}
}
requests lists the contact requests waiting for an
answer, newest first, and is absent when there are none. A device that follows
nothing yet loads the list after asking sync.state where its account stream
stands, as with block.list (see Following Streams).
Add Contact
Request:
{
"id": "31",
"type": "contact.add",
"data": {
"operation_id": "0195d1a0-add",
"user_id": 67890,
"nick": "New Friend"
}
}
Adding somebody who asked takes them in.
| Field | Type | Required | Description |
|---|---|---|---|
operation_id |
string | Yes | Idempotency key generated once for this logical operation |
user_id |
number | Yes | User's numeric ID |
nick |
string | No | Name for the entry; without one it takes the name the person goes by (see below) |
request |
boolean | No | true answers the person's request: it takes them in only while they still wait |
An answer (request: true) holds only while the request waits. Once it was
answered elsewhere — by another device that kept the question on screen — or
dropped by a block, the answer changes nothing and fails with
contact_not_found, which does not say why. Adding the same person twice in
two operations fails the second time with contact_exists.
Success:
{
"id": "31",
"type": "contact.add",
"seq": "42",
"ok": true,
"data": {
"contact": {"user_id": 67890, "nick": "New Friend", "avatar_hash": "a1b2c3d4"},
"presence": {"user_id": 67890, "online": true, "state": "available", "time": "2024-01-15T14:00:00Z"}
}
}
The answer, and the contact.added event of the owner's other devices, carry
the entry and the contact's presence as presence.get
would report it to the owner.
An entry nobody named — added without a nick, or a request —
takes the name the person goes by: their profile nickname, else their first and
last name, else their numeric ID, cut to 64 characters. It is a snapshot, as a
name in an address book is: the person renaming themselves later does not
rename them in anybody's list, and only the owner renames the entry.
Failure:
{
"id": "31",
"type": "contact.add",
"ok": false,
"error": {
"code": "user_not_found",
"message": "User does not exist"
}
}
Rename Contact
Request:
{
"id": "32",
"type": "contact.rename",
"data": {
"operation_id": "0195d1a0-rename",
"user_id": 67890,
"nick": "Best Friend"
}
}
Success:
{"id": "32", "type": "contact.rename", "seq": "43", "ok": true, "data": {"contact": {"user_id": 67890, "nick": "Best Friend", "avatar_hash": "a1b2c3d4"}}}
Delete Contact
Request:
{
"id": "33",
"type": "contact.delete",
"data": {
"operation_id": "0195d1a0-delete",
"user_id": 67890
}
}
Success:
{"id": "33", "type": "contact.delete", "seq": "44", "ok": true, "data": {"contact": {"user_id": 67890, "removed": true}}}
The same command turns a contact request away. Either way
the person is out of the list for good: no request and no address book brings
them back, only adding them by hand does. contact_not_found answers somebody
who is neither a contact nor a request.
With "request": true the command only answers the person's request, as
contact.add does: it turns them away while they still wait,
and fails with contact_not_found once the request was answered elsewhere or
dropped — a contact taken in meanwhile stays one.
Contact Events
{"type": "contact.renamed", "seq": "43", "data": {"contact": {"user_id": 67890, "nick": "Best Friend", "avatar_hash": "a1b2c3d4"}}}
{"type": "contact.deleted", "seq": "44", "data": {"contact": {"user_id": 67890, "removed": true}}}
contact.added carries the entry with the contact's presence, as the answer
to contact.add does; contact.renamed, contact.deleted and
contact.requested carry the entry alone. An event is read as its recipient
sees the person when it is delivered: the name the change gave the entry, and
the avatar and presence the person has then. A block takes the blocked person
out of the blocker's list without a contact.deleted of its own: clients
apply block.added to the list.
The other party hears nothing: a contact list is one person's, and being in somebody's list is not something the protocol reports.