18 KiB
06. Channels Read API
Человеко-читаемое объяснение
Эти функции — это чтение данных каналов для UI:
-
ListSubscriptionsFeed— отдает данные для экрана списка каналов:- ваши каналы (личный + созданные вами),
- каналы пользователей, на кого вы подписаны,
- отдельные каналы, на которые вы подписаны напрямую.
-
GetChannelMessages— отдает полную ленту одного канала (пока без курсоров, загружается сразу целиком), включая версии сообщений, лайки и ответы. -
GetMessageThread— отдает дерево обсуждения вокруг конкретного сообщения: предки, фокус-сообщение, потомки. -
GetMessageLikes— отдает списки пользователей, поставивших лайк сообщению, сгруппированные для UI по статусу профиля. -
GetPersonalDiary— отдает виртуальную лентуЛичный дневник, собранную изSTATUS_ACTIONтекущего пользователя. -
GetChannelsCounters— отдает счетчики разделов каналов для пользователя. -
SetChannelReadState— сохраняет подписанный watermark чтения канала и возвращает новый unread-счетчик. -
ListGroupChats200— отдает список групповых чатов типа200. -
GetGroupDialog— отдает сообщения конкретного группового чата типа200.
На первом этапе мы не используем курсоры (
nextCursor) и загружаем полные списки.
unreadCountдля канала считается по подписанному состоянию чтения канала. Для собственных каналов владельцу всегда возвращаетсяunreadCount = 0, чтобы его собственные публикации не становились «новыми» для него самого. Если для парыownerBlockchainName/channelNameещё нет записи, канал временно считается полностью прочитанным. После появления записи новые публикации увеличивают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,
"readCount": 0,
"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"
}
}
limit необязателен. Если поле отсутствует или равно 0, сервер возвращает всю ленту канала. Положительное значение ограничивает количество сообщений для совместимых клиентов.
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в текущей реализации не требуется: сервер возвращает полный найденный список лайков.- Пользователи группируются по состоянию профиля:
shining—account_role=primaryиshine_status=shining;official—account_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_hash—message.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_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
{
"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_fieldsuser_not_foundchannel_not_foundmessage_not_foundbad_limitchannel_name_already_existsCHANNEL_NOT_FOLLOWEDCHANNEL_NOT_FOUNDinternal_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 currentaccount_role=primary;shiningLikesCount— likes from users who are currently bothprimaryandshining.
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.