# 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 ```json { "op": "ListSubscriptionsFeed", "requestId": "req-1", "payload": { "login": "Alice", "limit": 200 } } ``` ### Response (success) ```json { "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 ```json { "op": "GetChannelMessages", "requestId": "req-2", "payload": { "channel": { "ownerBlockchainName": "bob-001", "channelRootBlockNumber": 123, "channelRootBlockHash": "..." }, "limit": 200, "sort": "asc" } } ``` ### Response (success) ```json { "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 ```json { "op": "GetMessageThread", "requestId": "req-3", "payload": { "message": { "blockchainName": "bob-001", "blockNumber": 333, "blockHash": "..." }, "depthUp": 20, "depthDown": 2, "limitChildrenPerNode": 50 } } ``` ### Response (success) ```json { "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` в текущей реализации не требуется: сервер возвращает полный найденный список лайков. - Пользователи группируются по состоянию профиля: - `shining` — `account_role=primary` и `shine_status=shining`; - `official` — `account_role=primary`, но без `shine_status=shining`; - `others` — остальные пользователи. ### Request ```json { "op": "GetMessageLikes", "requestId": "req-4", "payload": { "message": { "blockchainName": "bob-001", "blockNumber": 140, "blockHash": "..." } } } ``` ### Response (success) ```json { "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_hash` — `message.blockHash` не является корректным hex-хэшем блока. - `internal_error` — внутренняя ошибка чтения. `truncated` сейчас всегда `false`; поле оставлено в ответе для совместимости с UI и возможной будущей пагинацией. --- ## 5) GetPersonalDiary Возвращает виртуальный канал `Личный дневник` для самого пользователя. - Вызов доступен только владельцу дневника. - Сообщения в ответе строятся из блоков `STATUS_ACTION`. - Поля `targetMsgSubType`, `targetText`, `targetAuthorLogin`, `targetAuthorBlockchainName`, `targetCreatedAtMs` описывают исходный материал, к которому относится действие. ### Request ```json { "op": "GetPersonalDiary", "requestId": "req-4", "payload": { "login": "Alice", "limit": 200, "sort": "asc" } } ``` --- ## 6) GetChannelsCounters ### Request ```json { "op": "GetChannelsCounters", "requestId": "req-4", "payload": { "login": "Alice" } } ``` ### Response (success) ```json { "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 пользователя по строке: ```text SHiNe/ChannelReadState:|||| ``` ### Request ```json { "op": "SetChannelReadState", "requestId": "req-7", "payload": { "login": "Alice", "owner_bch_name": "bob-001", "channel_name": "news", "read_count": 90, "time_ms": 1760000000000, "client_key": "", "signature": "" } } ``` ### Response (success) ```json { "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_MISMATCH` — `login` не совпадает с текущей сессией. - `BAD_TIME` — время подписи слишком далеко в будущем. - `BAD_BASE64` — `client_key` или `signature` не являются корректным Base64. - `DEVICE_KEY_MISMATCH` — `client_key` не совпадает с текущим client key пользователя. - `INVALID_SIGNATURE` — подпись watermark не прошла проверку. - `CHANNEL_NOT_FOLLOWED` — пользователь не подписан на канал. - `CHANNEL_NOT_FOUND` — канал не найден. - `INTERNAL_ERROR` — внутренняя ошибка записи. --- ## 8) ListGroupChats200 ### Request ```json { "op": "ListGroupChats200", "requestId": "req-5", "payload": { "login": "Alice" } } ``` ### Response (success) ```json { "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 ```json { "op": "GetGroupDialog", "requestId": "req-6", "payload": { "login": "Alice", "group": { "ownerBlockchainName": "alice-001", "channelRootBlockNumber": 123 } } } ``` ### Response (success) ```json { "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. ```json { "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.