Skip to content
Unixgram

The API reference is currently available in Russian only. Method names, paths and examples are the same in every language.

Realtime

События в реальном времени — новые сообщения, прочтения, «печатает…», подарки — по постоянному WebSocket-соединению. Тот же канал, что у приложений.
Содержание

Обзор#

  • Адрес: wss://unixgram.com/unixproto. Кадры — двоичные сообщения WebSocket в MessagePack.
  • Порядок: получить токен через Client API → открыть сокет → hello → auth → дождаться auth_ok → получать realtime_event.
  • Канал только для получения. Действия — отправка, прочтение, реакции — выполняются обычными методами Client API.
  • Ботам сокет не нужен — у них getUpdates и вебхуки.

Подключение#

Токен#

GET /api/unixproto/token с cookie сессии → {"token", "expiresAt"}. Токен живёт 5 минут и нужен только для рукопожатия; на каждое переподключение берите новый.

HELLO и AUTH#

Первыми двумя кадрами клиент представляется и предъявляет токен:

json
{"version": 1, "id": "c1", "type": "hello", "timestamp": 1759500000000,
 "payload": {"client": "web", "appVersion": "my-client/1.0", "protocolVersion": 16, "layer": 1,
             "sessionId": "my-device-1",
             "init": {"deviceModel": "Raspberry Pi", "systemVersion": "Linux 6.6", "langCode": "ru", "systemLangCode": "ru"}}}

{"version": 1, "id": "c2", "type": "auth", "timestamp": 1759500000001, "payload": {"token": "<токен>"}}
ПолеТипОписание
clientStringweb, android, ios или server. Сторонним программам — web или server
appVersionStringНазвание и версия программы, до 32 символов
protocolVersionInteger15–17; документ описывает 16
sessionIdStringПостоянный идентификатор установки, до 64 символов
initObjectМодель, ОС и языки — видны в списке устройств, каждое поле до 64 символов
resumeObject{sessionId, lastSeq} — продолжить прежнюю сессию и дослать пропущенное после обрыва

Ответ — auth_ok. После него отправьте {"type": "presence_state", "payload": {"foreground": true}} — так вы будете «в сети», а не «был недавно».

Кадры#

ПолеТипОписание
versionIntegerВсегда 1
idStringУникальный идентификатор кадра
typeStringrealtime_event, ack, pong, error, …
timestampIntegerМиллисекунды Unix
seqIntegerНомер в журнале сессии — для msgs_ack и resume
importantBooleanКадр нужно подтвердить ack
payloadObjectУ realtime_event — {event, data, pts?, ptsCount?, boxKey?}
json
{"version": 1, "id": "s91", "type": "realtime_event", "timestamp": 1759500004210, "seq": 42,
 "payload": {"event": "message:new", "pts": 1759500004210123, "boxKey": "user:cmh1x…",
             "data": {"conversationId": "cmh7q…", "message": {"id": "cmh7r…", "content": "Привет", …}}}}

Подтверждения#

  • Кадр с important: true подтверждайте сразу: {"type": "ack", "ack": "<id кадра>", "payload": {}}. Неподтверждённый кадр сервер пришлёт снова.
  • Номера seq подтверждайте пачкой раз в секунду: {"type": "msgs_ack", "payload": {"seqs": [41, 42]}}.

Пинг#

Раз в 15 секунд отправляйте {"type": "ping_delay_disconnect", "payload": {"pingId": "…", "disconnectDelayMs": 45000}}: пинг и обещание молчать не дольше 45 секунд. Если от сервера ничего не приходило дольше этого времени, соединение мёртвое — переподключитесь.

Коды закрытия#

ПолеТипОписание
4401authТокен отвергнут — возьмите новый и переподключитесь
4403forbiddenДоступ запрещён
4413backpressureВы читали слишком медленно — переподключитесь и дочитайте пропущенное
4426protocol too oldВерсия протокола ниже 15 — переподключение не поможет
4429rate limitedСлишком часто — подождите 30 секунд

Переподключайтесь с паузой 1, 2, 4… секунды, не дольше 5 секунд при сетевых обрывах и 30 — при отказах сервера (коды 4xxx).

Пример#

Python: pip install websockets msgpack requests. Печатает новые сообщения.

python
import asyncio, time, uuid, msgpack, requests, websockets

API = "https://unixgram.com/api"
s = requests.Session()
s.get(f"{API}/auth/csrf")
s.post(f"{API}/auth/login", json={"email": "me@example.com", "password": "••••••••"},
       headers={"x-csrf-token": s.cookies["csrf_token"]})

def frame(type_, payload=None, **extra):
    body = {"version": 1, "id": uuid.uuid4().hex, "type": type_,
            "timestamp": int(time.time() * 1000), **extra}
    if payload is not None:
        body["payload"] = payload
    return msgpack.packb(body)

async def main():
    token = s.get(f"{API}/unixproto/token").json()["data"]["token"]
    async with websockets.connect("wss://unixgram.com/unixproto") as ws:
        await ws.send(frame("hello", {"client": "web", "appVersion": "printer/1.0", "protocolVersion": 16}))
        await ws.send(frame("auth", {"token": token}))
        async for raw in ws:
            f = msgpack.unpackb(raw)
            if f.get("important"):
                await ws.send(frame("ack", {}, ack=f["id"]))
            if f.get("seq") is not None:
                await ws.send(frame("msgs_ack", {"seqs": [f["seq"]]}))
            if f["type"] == "auth_ok":
                await ws.send(frame("presence_state", {"foreground": True}))
            if f["type"] == "realtime_event" and f["payload"]["event"] == "message:new":
                message = f["payload"]["data"]["message"]
                print(message["sender"]["username"], ":", message["content"])

asyncio.run(main())

События#

durable — у события есть строка в базе и номер в журнале (pts): пропущенное можно дочитать. ephemeral — доставляется и забывается (набор текста, присутствие, счётчики): пропустили — перечитайте состояние обычным методом.

eventКлассКогда
message:newdurableНовое сообщение в чате: conversationId, message, selfEcho (ваше же сообщение с другого устройства)
message:deleteddurableСообщения удалены: conversationId, messageIds
message:readephemeralСобеседник прочитал ваши сообщения до момента at
message:typingephemeralКто-то печатает: conversationId, userId
conversation:updateephemeralЧат изменился (правка, реакция, закреп) — перечитайте его
counter:unreadephemeralИзменилось число непрочитанных
notification:newdurableНовое уведомление
notification:readephemeralУведомления прочитаны на другом устройстве
channel:post:newdurableНовый пост в канале, на который вы подписаны
channel:commentsephemeralНовый или удалённый комментарий в открытой вами ветке: conversationId, postId
comment:newdurableНовый комментарий к вашему посту
post:updatedephemeralПост изменился (лайки, правка)
feed:newephemeralВ ленте есть новые посты
user:followedephemeralНа вас подписались
user:updatedephemeralПрофиль пользователя изменился
gift:receiveddurableВам подарили подарок
gift:offerdurableПредложение цены за ваш подарок
gift:updatedephemeralПодарок изменился (продажа, улучшение)
stars:balanceephemeralИзменился баланс звёзд
streak:updateephemeralИзменилась серия
call:incomingephemeralВходящий звонок
call:accepted · call:declined · call:busy · call:endedephemeralСостояние звонка
call:signalephemeralСигнализация WebRTC (SDP, ICE)
secret:request · secret:accept · secret:reject · secret:closedurableЖизненный цикл секретного чата
secret:message · secret:read · secret:deleteddurableСообщения секретного чата (шифротекст)
secret:typingephemeralПечатает в секретном чате
bot:callbackAnswerephemeralОтвет бота на нажатие кнопки: текст подсказки или окна

Список событий может пополняться — пропускайте незнакомые имена.

Пропущенные события#

У durable-событий есть pts и boxKey — номер в журнале и имя журнала (ваш личный, канал и т. п.). Запоминайте последний pts каждого журнала; после обрыва дочитайте разницу:

  1. GET /api/updates/state → {"boxes": [{"boxKey", "pts"}, …]} — с чего начинать при первом входе.
  2. GET /api/updates/difference?box=<boxKey>&pts=<последний> → один из ответов:
    • state — ничего не пропущено;
    • difference / differenceSlice — entries пропущенных событий; у slice повторите запрос с новым pts;
    • differenceTooLong — пропущено больше, чем хранит журнал (512 записей или 48 часов): перечитайте чаты и уведомления обычными методами.