SHA256
448 lines
13 KiB
Markdown
448 lines
13 KiB
Markdown
# 06. Channels Read API
|
||
|
||
## Человеко-читаемое объяснение
|
||
Эти функции — это **чтение данных каналов** для UI:
|
||
|
||
1. `ListSubscriptionsFeed` — отдает данные для экрана списка каналов:
|
||
- ваши каналы (личный + созданные вами),
|
||
- каналы пользователей, на кого вы подписаны,
|
||
- отдельные каналы, на которые вы подписаны напрямую.
|
||
|
||
2. `GetChannelMessages` — отдает полную ленту одного канала (пока без курсоров, загружается сразу целиком),
|
||
включая версии сообщений, лайки и ответы.
|
||
|
||
3. `GetMessageThread` — отдает дерево обсуждения вокруг конкретного сообщения:
|
||
предки, фокус-сообщение, потомки.
|
||
|
||
4. `GetPersonalDiary` — отдает виртуальную ленту `Личный дневник`, собранную из `STATUS_ACTION` текущего пользователя.
|
||
|
||
5. `GetChannelsCounters` — отдает счетчики разделов каналов для пользователя.
|
||
|
||
6. `ListGroupChats200` — отдает список групповых чатов типа `200`.
|
||
|
||
7. `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,
|
||
"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) GetPersonalDiary
|
||
|
||
Возвращает виртуальный канал `Личный дневник` для самого пользователя.
|
||
|
||
- Вызов доступен только владельцу дневника.
|
||
- Сообщения в ответе строятся из блоков `STATUS_ACTION`.
|
||
- Поля `targetMsgSubType`, `targetText`, `targetAuthorLogin`, `targetAuthorBlockchainName`, `targetCreatedAtMs` описывают исходный материал, к которому относится действие.
|
||
|
||
### Request
|
||
```json
|
||
{
|
||
"op": "GetPersonalDiary",
|
||
"requestId": "req-4",
|
||
"payload": {
|
||
"login": "Alice",
|
||
"limit": 200,
|
||
"sort": "asc"
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 4) 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
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5) 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
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6) 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`
|
||
- `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.
|