Files
SHiNE-server/docs/API/06_Channels_Read_API.md
T

17 KiB
Raw Blame History

06. Channels Read API

Человеко-читаемое объяснение

Эти функции — это чтение данных каналов для UI:

  1. ListSubscriptionsFeed — отдает данные для экрана списка каналов:

    • ваши каналы (личный + созданные вами),
    • каналы пользователей, на кого вы подписаны,
    • отдельные каналы, на которые вы подписаны напрямую.
  2. GetChannelMessages — отдает полную ленту одного канала (пока без курсоров, загружается сразу целиком), включая версии сообщений, лайки и ответы.

  3. GetMessageThread — отдает дерево обсуждения вокруг конкретного сообщения: предки, фокус-сообщение, потомки.

  4. GetMessageLikes — отдает списки пользователей, поставивших лайк сообщению, сгруппированные для UI по статусу профиля.

  5. GetPersonalDiary — отдает виртуальную ленту Личный дневник, собранную из STATUS_ACTION текущего пользователя.

  6. GetChannelsCounters — отдает счетчики разделов каналов для пользователя.

  7. SetChannelReadState — сохраняет подписанный watermark чтения канала и возвращает новый unread-счетчик.

  8. ListGroupChats200 — отдает список групповых чатов типа 200.

  9. GetGroupDialog — отдает сообщения конкретного группового чата типа 200.

На первом этапе мы не используем курсоры (nextCursor) и загружаем полные списки.

unreadCount для канала считается по user_settings. Для собственных каналов владельцу всегда возвращается unreadCount = 0, чтобы его собственные публикации не становились «новыми» для него самого. Если для пары ownerBlockchainName/channelName ещё нет записи, канал временно считается полностью прочитанным; UI при загрузке списка каналов создаёт baseline на текущем messagesCount. После этого новые публикации увеличивают unreadCount до продвижения курсора чтения.


1) ListSubscriptionsFeed

Request

{
  "op": "ListSubscriptionsFeed",
  "requestId": "req-1",
  "payload": {
    "login": "Alice",
    "limit": 200
  }
}

Response (success)

{
  "op": "ListSubscriptionsFeed",
  "requestId": "req-1",
  "status": 200,
  "ok": true,
  "payload": {
    "login": "Alice",
    "ownedChannels": [
      {
        "channel": {
          "ownerLogin": "Alice",
          "ownerBlockchainName": "alice-001",
          "channelName": "0",
          "displayName": "Мой канал",
          "channelDescription": "Короткое описание",
          "avaAr": "ArweaveTxId...",
          "avaSha256": "0123...",
          "avaSize": 248193,
          "metaUpdatedAtMs": 1760000000000,
          "personal": true,
          "channelRoot": { "blockNumber": 0, "blockHash": "..." }
        },
        "messagesCount": 120,
        "lastMessage": {
          "messageRef": { "blockNumber": 921, "blockHash": "..." },
          "text": "последняя версия текста",
          "createdAtMs": 1760000000000,
          "authorLogin": "Alice",
          "authorBlockchainName": "alice-001"
        }
      }
    ],
    "followedUsersChannels": [
      {
        "channel": {
          "ownerLogin": "Bob",
          "ownerBlockchainName": "bob-001",
          "channelName": "0",
          "personal": true,
          "channelRoot": { "blockNumber": 0, "blockHash": "..." }
        },
        "messagesCount": 540,
        "lastMessage": {
          "messageRef": { "blockNumber": 922, "blockHash": "..." },
          "text": "последняя версия текста",
          "createdAtMs": 1760000100000,
          "authorLogin": "Bob",
          "authorBlockchainName": "bob-001"
        }
      }
    ],
    "followedChannels": [
      {
        "channel": {
          "ownerLogin": "Carl",
          "ownerBlockchainName": "carl-001",
          "channelName": "market",
          "personal": false,
          "channelRoot": { "blockNumber": 456, "blockHash": "..." }
        },
        "messagesCount": 90,
        "unreadCount": 0,
        "readStateInitialized": false,
        "lastMessage": {
          "messageRef": { "blockNumber": 1002, "blockHash": "..." },
          "text": "актуальный текст",
          "createdAtMs": 1760001000000,
          "authorLogin": "Carl",
          "authorBlockchainName": "carl-001"
        }
      }
    ]
  }
}

2) GetChannelMessages

Request

{
  "op": "GetChannelMessages",
  "requestId": "req-2",
  "payload": {
    "channel": {
      "ownerBlockchainName": "bob-001",
      "channelRootBlockNumber": 123,
      "channelRootBlockHash": "..."
    },
    "limit": 200,
    "sort": "asc"
  }
}

Response (success)

{
  "op": "GetChannelMessages",
  "requestId": "req-2",
  "status": 200,
  "ok": true,
  "payload": {
    "channel": {
      "ownerLogin": "Bob",
      "ownerBlockchainName": "bob-001",
      "channelName": "news",
      "displayName": "Новости Bob",
      "channelDescription": "Канал о новостях",
      "avaAr": "ArweaveTxId...",
      "avaSha256": "0123...",
      "avaSize": 248193,
      "metaUpdatedAtMs": 1760000000000,
      "subscribersCount": 128,
      "channelRoot": { "blockNumber": 123, "blockHash": "..." }
    },
    "metaEvents": [
      {
        "kind": "created",
        "messageRef": { "blockNumber": 123, "blockHash": "..." },
        "authorLogin": "Bob",
        "authorBlockchainName": "bob-001",
        "lineStep": 1,
        "createdAtMs": 1760000000000,
        "title": "news",
        "description": "",
        "avaAr": "",
        "avaSha256": "",
        "avaSize": 0
      },
      {
        "kind": "profile_updated",
        "messageRef": { "blockNumber": 130, "blockHash": "..." },
        "authorLogin": "Bob",
        "authorBlockchainName": "bob-001",
        "lineStep": 2,
        "createdAtMs": 1760000500000,
        "title": "Новости Bob",
        "description": "Канал о новостях",
        "avaAr": "ArweaveTxId...",
        "avaSha256": "0123...",
        "avaSize": 248193
      }
    ],
    "messages": [
      {
        "messageRef": { "blockNumber": 140, "blockHash": "..." },
        "authorLogin": "Bob",
        "authorBlockchainName": "bob-001",
        "createdAtMs": 1760000000000,
        "text": "текущая версия",
        "likesCount": 12,
        "repliesCount": 3,
        "ratingsCount": 2,
        "versionsTotal": 4,
        "versions": [
          { "versionIndex": 1, "blockNumber": 140, "blockHash": "...", "text": "v1", "createdAtMs": 1760000000000 },
          { "versionIndex": 2, "blockNumber": 155, "blockHash": "...", "text": "v2", "createdAtMs": 1760001000000 },
          { "versionIndex": 3, "blockNumber": 170, "blockHash": "...", "text": "v3", "createdAtMs": 1760002000000 },
          { "versionIndex": 4, "blockNumber": 199, "blockHash": "...", "text": "v4", "createdAtMs": 1760003000000 }
        ]
      }
    ]
  }
}

3) GetMessageThread

Request

{
  "op": "GetMessageThread",
  "requestId": "req-3",
  "payload": {
    "message": {
      "blockchainName": "bob-001",
      "blockNumber": 333,
      "blockHash": "..."
    },
    "depthUp": 20,
    "depthDown": 2,
    "limitChildrenPerNode": 50
  }
}

Response (success)

{
  "op": "GetMessageThread",
  "requestId": "req-3",
  "status": 200,
  "ok": true,
  "payload": {
    "ancestors": [MessageNode],
    "focus": MessageNode,
    "descendants": [MessageNodeTree]
  }
}

MessageNode (дополнение)

  • MessageNode расширяет формат сообщения из GetChannelMessages и дополнительно содержит:
    • channelInfo — мета-информация о канале (если применимо);
    • rawBlockB64 — сырой block_bytes текущего блока в Base64.
  • Поле rawBlockB64 присутствует у узлов во всех частях ответа GetMessageThread: focus, ancestors[], descendants[].
  • В GetChannelMessages поле rawBlockB64 не добавляется (лента канала без сырого блока, чтобы не раздувать ответ).
  • И в GetChannelMessages, и в GetMessageThread каждое сообщение теперь содержит:
    • repliesCount — число дочерних сообщений типа TEXT_REPLY;
    • ratingsCount — число дочерних сообщений типа TEXT_RATING.
  • В descendants[] операции GetMessageThread возвращаются оба типа дочерних текстовых сообщений:
    • TEXT_REPLY;
    • TEXT_RATING. Они идут в одной общей ветке обсуждения и сортируются по времени создания.

4) GetMessageLikes

Возвращает пользователей, которые поставили лайк конкретному сообщению канала.

  • message.blockchainName, message.blockNumber, message.blockHash должны указывать на исходное сообщение.
  • limit в текущей реализации не требуется: сервер возвращает полный найденный список лайков.
  • Пользователи группируются по состоянию профиля:
    • shiningaccount_role=primary и shine_status=shining;
    • officialaccount_role=primary, но без shine_status=shining;
    • others — остальные пользователи.

Request

{
  "op": "GetMessageLikes",
  "requestId": "req-4",
  "payload": {
    "message": {
      "blockchainName": "bob-001",
      "blockNumber": 140,
      "blockHash": "..."
    }
  }
}

Response (success)

{
  "op": "GetMessageLikes",
  "requestId": "req-4",
  "status": 200,
  "ok": true,
  "payload": {
    "shining": [
      { "login": "Alice", "firstName": "Alice", "lastName": "", "avatarAr": "ArweaveTxId..." }
    ],
    "official": [],
    "others": [
      { "login": "Carl", "firstName": "", "lastName": "", "avatarAr": "" }
    ],
    "total": 2,
    "truncated": false
  }
}

Ошибки

  • bad_fields — не передан message или обязательные поля ссылки на сообщение.
  • bad_hashmessage.blockHash не является корректным hex-хэшем блока.
  • internal_error — внутренняя ошибка чтения.

truncated сейчас всегда false; поле оставлено в ответе для совместимости с UI и возможной будущей пагинацией.


5) GetPersonalDiary

Возвращает виртуальный канал Личный дневник для самого пользователя.

  • Вызов доступен только владельцу дневника.
  • Сообщения в ответе строятся из блоков STATUS_ACTION.
  • Поля targetMsgSubType, targetText, targetAuthorLogin, targetAuthorBlockchainName, targetCreatedAtMs описывают исходный материал, к которому относится действие.

Request

{
  "op": "GetPersonalDiary",
  "requestId": "req-4",
  "payload": {
    "login": "Alice",
    "limit": 200,
    "sort": "asc"
  }
}

6) GetChannelsCounters

Request

{
  "op": "GetChannelsCounters",
  "requestId": "req-4",
  "payload": {
    "login": "Alice"
  }
}

Response (success)

{
  "op": "GetChannelsCounters",
  "requestId": "req-4",
  "status": 200,
  "ok": true,
  "payload": {
    "login": "Alice",
    "feedCount": 12,
    "dialogs100Count": 3,
    "groupChats200Count": 4,
    "myChannelsCount": 2
  }
}

7) SetChannelReadState

Сохраняет подписанную позицию чтения для одного канала, на который пользователь подписан.

  • Требует авторизованное WebSocket-соединение.
  • login должен совпадать с текущим авторизованным пользователем.
  • Подпись строится client key пользователя по строке:
SHiNe/ChannelReadState:<login>|<owner_bch_name>|<channel_name>|<time_ms>|<read_count>

Request

{
  "op": "SetChannelReadState",
  "requestId": "req-7",
  "payload": {
    "login": "Alice",
    "owner_bch_name": "bob-001",
    "channel_name": "news",
    "read_count": 90,
    "time_ms": 1760000000000,
    "client_key": "<base64-client-public-key>",
    "signature": "<base64-ed25519-signature>"
  }
}

Response (success)

{
  "op": "SetChannelReadState",
  "requestId": "req-7",
  "status": 200,
  "ok": true,
  "payload": {
    "login": "Alice",
    "owner_bch_name": "bob-001",
    "channel_name": "news",
    "read_count": 90,
    "unread_count": 0,
    "time_ms": 1760000000000,
    "applied": true
  }
}

Ошибки

  • NOT_AUTHENTICATED — нет авторизованной сессии.
  • BAD_FIELDS — не переданы обязательные поля.
  • LOGIN_MISMATCHlogin не совпадает с текущей сессией.
  • BAD_TIME — время подписи слишком далеко в будущем.
  • BAD_BASE64client_key или signature не являются корректным Base64.
  • DEVICE_KEY_MISMATCHclient_key не совпадает с текущим client key пользователя.
  • INVALID_SIGNATURE — подпись watermark не прошла проверку.
  • CHANNEL_NOT_FOLLOWED — пользователь не подписан на канал.
  • CHANNEL_NOT_FOUND — канал не найден.
  • INTERNAL_ERROR — внутренняя ошибка записи.

8) ListGroupChats200

Request

{
  "op": "ListGroupChats200",
  "requestId": "req-5",
  "payload": {
    "login": "Alice"
  }
}

Response (success)

{
  "op": "ListGroupChats200",
  "requestId": "req-5",
  "status": 200,
  "ok": true,
  "payload": {
    "login": "Alice",
    "chats": [
      {
        "ownerLogin": "Alice",
        "ownerBlockchainName": "alice-001",
        "channelRootBlockNumber": 123,
        "channelRootBlockHash": "...",
        "channelName": "team",
        "chatTitle": "Team chat",
        "membersCount": 3,
        "updatedAtMs": 1760000000000
      }
    ]
  }
}

9) GetGroupDialog

Request

{
  "op": "GetGroupDialog",
  "requestId": "req-6",
  "payload": {
    "login": "Alice",
    "group": {
      "ownerBlockchainName": "alice-001",
      "channelRootBlockNumber": 123
    }
  }
}

Response (success)

{
  "op": "GetGroupDialog",
  "requestId": "req-6",
  "status": 200,
  "ok": true,
  "payload": {
    "group": {
      "ownerLogin": "Alice",
      "ownerBlockchainName": "alice-001",
      "channelRootBlockNumber": 123,
      "channelName": "team",
      "chatTitle": "Team chat"
    },
    "messages": [
      {
        "authorLogin": "Bob",
        "authorBlockchainName": "bob-001",
        "blockNumber": 140,
        "blockHash": "...",
        "createdAtMs": 1760000000000,
        "text": "Привет"
      }
    ]
  }
}

Reason codes

  • bad_fields
  • user_not_found
  • channel_not_found
  • message_not_found
  • limit_too_large
  • channel_name_already_exists
  • CHANNEL_NOT_FOLLOWED
  • CHANNEL_NOT_FOUND
  • internal_error

Profile public-channel lists (v18)

ListUserProfileChannels

Returns lightweight public-channel cards for profile counters. mode is owned or following; only channel_type_code=1 is returned.

{
  "op": "ListUserProfileChannels",
  "requestId": "profile-ch-1",
  "payload": { "login": "Alice", "mode": "owned", "limit": 100, "offset": 0 }
}

Channel items contain ownerLogin, slug, displayName, avatarAr, ownerBlockchainName, rootBlockNumber, and rootBlockHashHex.

Qualified likes (v18)

Channel/message responses expose three counters:

  • likesCount — all active likes;
  • primaryLikesCount — likes from users whose current account_role=primary;
  • shiningLikesCount — likes from users who are currently both primary and shining.

Changing account_role or shine does not alter the raw reaction. The two qualified counters are recalculated from reactions_state; therefore old likes automatically enter or leave qualified counters when the actor's current status changes.