From 9723696b2ca2d3274359ee683941ad41e7b17222cd0770f3d7c39a7215a335f3 Mon Sep 17 00:00:00 2001 From: ai5590 Date: Mon, 30 Mar 2026 14:32:15 +0300 Subject: [PATCH] Start server-side channel read RPC handlers and simplify API spec --- Dev_Docs/API/06_Channels_Read_API.md | 208 +++++++++++++++ shine-UI/js/app.js | 2 + shine-UI/js/mock-data.js | 77 ++++-- shine-UI/js/pages/add-channel-view.js | 38 +++ shine-UI/js/pages/channel-view.js | 9 +- shine-UI/js/pages/channels-list.js | 136 ++++++++-- shine-UI/js/router.js | 2 +- shine-UI/styles/components.css | 35 +++ .../ws_protocol/JSON/JsonHandlerRegistry.java | 12 + .../blockchain/Net_AddBlock_Handler.java | 43 ++++ .../channels/ChannelsReadSupport.java | 240 ++++++++++++++++++ .../Net_GetChannelMessages_Handler.java | 115 +++++++++ .../Net_GetMessageThread_Handler.java | 224 ++++++++++++++++ .../Net_ListSubscriptionsFeed_Handler.java | 168 ++++++++++++ .../Net_GetChannelMessages_Request.java | 33 +++ .../Net_GetChannelMessages_Response.java | 109 ++++++++ .../Net_GetMessageThread_Request.java | 37 +++ .../Net_GetMessageThread_Response.java | 50 ++++ .../Net_ListSubscriptionsFeed_Request.java | 14 + .../Net_ListSubscriptionsFeed_Response.java | 97 +++++++ 20 files changed, 1604 insertions(+), 45 deletions(-) create mode 100644 Dev_Docs/API/06_Channels_Read_API.md create mode 100644 shine-UI/js/pages/add-channel-view.js create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetMessageThread_Handler.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_ListSubscriptionsFeed_Handler.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Request.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Response.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetMessageThread_Request.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetMessageThread_Response.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Request.java create mode 100644 shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Response.java diff --git a/Dev_Docs/API/06_Channels_Read_API.md b/Dev_Docs/API/06_Channels_Read_API.md new file mode 100644 index 0000000..60d390c --- /dev/null +++ b/Dev_Docs/API/06_Channels_Read_API.md @@ -0,0 +1,208 @@ +# 06. Channels Read API + +## Человеко-читаемое объяснение +Эти 3 функции — это **чтение данных каналов** для UI: + +1. `ListSubscriptionsFeed` — отдает данные для экрана списка каналов: + - ваши каналы (личный + созданные вами), + - каналы пользователей, на кого вы подписаны, + - отдельные каналы, на которые вы подписаны напрямую. + +2. `GetChannelMessages` — отдает полную ленту одного канала (пока без курсоров, загружается сразу целиком), + включая версии сообщений, лайки и ответы. + +3. `GetMessageThread` — отдает дерево обсуждения вокруг конкретного сообщения: + предки, фокус-сообщение, потомки. + +> На первом этапе мы **не используем курсоры** (`nextCursor`) и загружаем полные списки. + +--- + +## 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", + "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", + "channelRoot": { "blockNumber": 123, "blockHash": "..." } + }, + "messages": [ + { + "messageRef": { "blockNumber": 140, "blockHash": "..." }, + "authorLogin": "Bob", + "authorBlockchainName": "bob-001", + "createdAtMs": 1760000000000, + "text": "текущая версия", + "likesCount": 12, + "repliesCount": 3, + "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] + } +} +``` + +--- + +## Reason codes +- `bad_fields` +- `user_not_found` +- `channel_not_found` +- `message_not_found` +- `limit_too_large` +- `channel_name_already_exists` +- `internal_error` diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 5dfaad7..080b679 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -39,6 +39,7 @@ import * as contactSearchView from './pages/contact-search-view.js?v=20260330001 import * as chatView from './pages/chat-view.js?v=20260330001044'; import * as channelsList from './pages/channels-list.js?v=20260330001044'; import * as channelView from './pages/channel-view.js?v=20260330001044'; +import * as addChannelView from './pages/add-channel-view.js?v=20260330001044'; import * as networkView from './pages/network-view.js?v=20260330001044'; import * as notificationsView from './pages/notifications-view.js?v=20260330001044'; @@ -69,6 +70,7 @@ const routes = { 'chat-view': chatView, 'channels-list': channelsList, 'channel-view': channelView, + 'add-channel-view': addChannelView, 'network-view': networkView, 'notifications-view': notificationsView, }; diff --git a/shine-UI/js/mock-data.js b/shine-UI/js/mock-data.js index 82dc24b..87b11bd 100644 --- a/shine-UI/js/mock-data.js +++ b/shine-UI/js/mock-data.js @@ -147,35 +147,80 @@ export const chatMessages = { export const channels = [ { - id: 'ch1', - name: 'Новости продукта', - initials: 'НП', - description: 'Официальный канал команды Shine с релизами и обновлениями.', - lastMessage: 'Опубликовали обзор нового демо-прототипа мобильного интерфейса.', + id: 'ch0', + name: 'Личный канал', + initials: 'ЛК', + ownerLogin: '@shine.alex', + ownerName: 'Вы', + description: 'Ваш основной канал (нулевой).', + lastMessage: 'Добро пожаловать в личный канал.', time: '16:05', - unread: 14, + messagesCount: 14, + kind: 'own-personal', + }, + { + id: 'ch1', + name: 'Команда продукта', + initials: 'КП', + ownerLogin: '@shine.alex', + ownerName: 'Вы', + description: 'Канал команды, который вы создали.', + lastMessage: 'Обновили roadmap на апрель.', + time: '15:42', + messagesCount: 8, + kind: 'own', }, { id: 'ch2', - name: 'Анекдоты дня', - initials: 'АД', - description: 'Лёгкий развлекательный канал с короткими шутками и мемами.', - lastMessage: 'Новый пост: как дизайнер, разработчик и дедлайн зашли в бар.', + name: 'Новости Bob', + initials: 'NB', + ownerLogin: '@bob', + ownerName: 'Bob', + description: 'Основной канал пользователя Bob.', + lastMessage: 'Вышел новый дайджест разработчика.', time: '15:20', - unread: 3, + messagesCount: 5, + kind: 'followed-user-channel', }, { id: 'ch3', - name: 'Новости рынка', - initials: 'НР', - description: 'Короткие ежедневные сводки по рынку, технологиям и сообществам.', - lastMessage: 'В ленте свежая подборка новостей и главных событий дня.', + name: 'Стендап команды Bob', + initials: 'SB', + ownerLogin: '@bob', + ownerName: 'Bob', + description: 'Второй канал пользователя Bob.', + lastMessage: 'Перенесли созвон на 19:30.', time: 'вчера', - unread: 0, + messagesCount: 11, + kind: 'followed-user-channel', + }, + { + id: 'ch4', + name: 'Анекдоты дня', + initials: 'АД', + ownerLogin: '@fun.club', + ownerName: 'Fun Club', + description: 'Публичный развлекательный канал по подписке.', + lastMessage: 'Сегодня в выпуске 5 новых шуток.', + time: 'вчера', + messagesCount: 33, + kind: 'subscribed', }, ]; export const channelPosts = { + ch0: [ + { + id: 'p0-1', + title: 'Первый личный пост', + body: 'Этот канал всегда ваш и стоит в списке первым.', + }, + { + id: 'p0-2', + title: 'Планы', + body: 'Сюда удобно сохранять личные заметки и объявления.', + }, + ], ch1: [ { id: 'p1', diff --git a/shine-UI/js/pages/add-channel-view.js b/shine-UI/js/pages/add-channel-view.js new file mode 100644 index 0000000..a9238f9 --- /dev/null +++ b/shine-UI/js/pages/add-channel-view.js @@ -0,0 +1,38 @@ +import { renderHeader } from '../components/header.js?v=20260330001044'; + +export const pageMeta = { id: 'add-channel-view', title: 'Добавить канал' }; + +export function render({ navigate }) { + const screen = document.createElement('section'); + screen.className = 'stack'; + + screen.append( + renderHeader({ + title: 'Добавить канал', + leftAction: { label: '←', onClick: () => navigate('channels-list') }, + }) + ); + + const form = document.createElement('form'); + form.className = 'card stack'; + form.innerHTML = ` + + +
+ + +
+ `; + + form.addEventListener('submit', (event) => { + event.preventDefault(); + navigate('channels-list'); + }); + + form.querySelector('#cancel-create-channel').addEventListener('click', () => { + navigate('channels-list'); + }); + + screen.append(form); + return screen; +} diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js index 6a7ca1c..d7fb07a 100644 --- a/shine-UI/js/pages/channel-view.js +++ b/shine-UI/js/pages/channel-view.js @@ -7,6 +7,7 @@ export function render({ navigate, route }) { const channelId = route.params.channelId || 'ch1'; const channel = channels.find((c) => c.id === channelId) || channels[0]; const posts = channelPosts[channelId] || []; + const isOwnChannel = channel.ownerLogin === '@shine.alex'; const screen = document.createElement('section'); screen.className = 'stack'; @@ -23,9 +24,13 @@ export function render({ navigate, route }) { head.innerHTML = ` # ${channel.name}

${channel.description}

-

Публичный канал, режим только чтение

+

Владелец: ${channel.ownerName}

`; + const actionButton = document.createElement('button'); + actionButton.className = isOwnChannel ? 'primary-btn' : 'secondary-btn'; + actionButton.textContent = isOwnChannel ? 'Добавить сообщение в канал' : 'Отписаться от канала'; + const feed = document.createElement('div'); feed.className = 'stack'; @@ -36,6 +41,6 @@ export function render({ navigate, route }) { feed.append(card); }); - screen.append(head, feed); + screen.append(head, actionButton, feed); return screen; } diff --git a/shine-UI/js/pages/channels-list.js b/shine-UI/js/pages/channels-list.js index c8768a0..8d335c1 100644 --- a/shine-UI/js/pages/channels-list.js +++ b/shine-UI/js/pages/channels-list.js @@ -3,40 +3,124 @@ import { channels } from '../mock-data.js?v=20260330001044'; export const pageMeta = { id: 'channels-list', title: 'Каналы' }; +function openSimpleSubscribeModal(kindLabel) { + const root = document.getElementById('modal-root'); + root.innerHTML = ` + + `; + + const close = () => { + root.innerHTML = ''; + }; + + root.querySelector('#sub-cancel').addEventListener('click', close); + root.querySelector('#sub-submit').addEventListener('click', close); +} + +function renderChannelRow(channel, navigate) { + const row = document.createElement('article'); + row.className = 'list-item'; + row.innerHTML = ` +
${channel.initials}
+
+ # ${channel.name} +

${channel.description}

+

${channel.lastMessage}

+

Владелец: ${channel.ownerName}

+
+
+ Канал + ${channel.time} + ${channel.messagesCount} +
+ `; + row.addEventListener('click', () => navigate(`channel-view/${channel.id}`)); + return row; +} + +function renderSection(title, items, navigate) { + const wrap = document.createElement('section'); + wrap.className = 'stack'; + + const header = document.createElement('h3'); + header.className = 'section-title'; + header.textContent = title; + + wrap.append(header); + + items.forEach((channel) => { + wrap.append(renderChannelRow(channel, navigate)); + }); + + return wrap; +} + export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; - screen.append(renderHeader({ title: 'Каналы' })); + screen.append( + renderHeader({ + title: 'Каналы', + rightActions: [ + { label: 'Подписаться на человека', onClick: () => openSimpleSubscribeModal('Подписка на человека') }, + { label: 'Подписаться на канал', onClick: () => openSimpleSubscribeModal('Подписка на канал') }, + ], + }) + ); - const search = document.createElement('div'); - search.className = 'card'; - search.textContent = 'Найти канал'; - search.style.color = 'var(--text-muted)'; + const ownChannels = channels + .filter((channel) => channel.kind === 'own-personal' || channel.kind === 'own') + .sort((a, b) => { + if (a.kind === 'own-personal') return -1; + if (b.kind === 'own-personal') return 1; + return a.name.localeCompare(b.name, 'ru'); + }); + + const followedUserChannels = channels.filter((channel) => channel.kind === 'followed-user-channel'); + const subscribedChannels = channels.filter((channel) => channel.kind === 'subscribed'); + + const listWrap = document.createElement('div'); + listWrap.className = 'channels-scroll-wrap'; const list = document.createElement('div'); - list.className = 'stack'; + list.className = 'stack channels-groups'; - channels.forEach((channel) => { - const row = document.createElement('article'); - row.className = 'list-item'; - row.innerHTML = ` -
${channel.initials}
-
- # ${channel.name} -

${channel.description}

-

${channel.lastMessage}

-
-
- Канал - ${channel.time} - ${channel.unread ? `${channel.unread}` : ''} -
- `; - row.addEventListener('click', () => navigate(`channel-view/${channel.id}`)); - list.append(row); - }); + list.append(renderSection('Мои каналы', ownChannels, navigate)); - screen.append(search, list); + const dividerOne = document.createElement('hr'); + dividerOne.className = 'channels-divider'; + list.append(dividerOne); + + list.append(renderSection('Каналы пользователей, на кого вы подписаны', followedUserChannels, navigate)); + + const dividerTwo = document.createElement('hr'); + dividerTwo.className = 'channels-divider'; + list.append(dividerTwo); + + list.append(renderSection('Каналы, на которые вы подписаны', subscribedChannels, navigate)); + + const addChannelButton = document.createElement('button'); + addChannelButton.className = 'primary-btn'; + addChannelButton.textContent = 'Добавить канал'; + addChannelButton.addEventListener('click', () => navigate('add-channel-view')); + + list.append(addChannelButton); + + const scrollHint = document.createElement('div'); + scrollHint.className = 'channels-scroll-hint'; + + listWrap.append(list, scrollHint); + screen.append(listWrap); return screen; } diff --git a/shine-UI/js/router.js b/shine-UI/js/router.js index 6569141..78a339e 100644 --- a/shine-UI/js/router.js +++ b/shine-UI/js/router.js @@ -57,6 +57,6 @@ export function resolveToolbarActive(pageId) { return 'profile-view'; } if (pageId === 'chat-view' || pageId === 'contact-search-view') return 'messages-list'; - if (pageId === 'channel-view') return 'channels-list'; + if (pageId === 'channel-view' || pageId === 'add-channel-view') return 'channels-list'; return 'profile-view'; } diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index 03c64a3..239c62b 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -728,3 +728,38 @@ border-radius: 16px; padding: 14px; } + +.section-title { + font-size: 14px; + font-weight: 700; + color: #dbe7ff; + margin: 4px 2px; +} + +.channels-scroll-wrap { + position: relative; + max-height: 58vh; + overflow-y: auto; + padding-right: 12px; +} + +.channels-groups { + min-height: min-content; +} + +.channels-divider { + border: 0; + border-top: 1px solid rgba(255, 255, 255, 0.14); + margin: 6px 0 8px; +} + +.channels-scroll-hint { + position: absolute; + top: 4px; + right: 0; + width: 4px; + height: calc(100% - 8px); + border-radius: 999px; + background: linear-gradient(180deg, rgba(83, 216, 251, 0.55), rgba(83, 216, 251, 0.15)); + pointer-events: none; +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java index 4f608cb..b39278c 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/JsonHandlerRegistry.java @@ -45,6 +45,12 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUser // --- NEW: connections friends lists --- import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Handler; import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriendsLists_Request; +import server.logic.ws_protocol.JSON.handlers.channels.Net_GetChannelMessages_Handler; +import server.logic.ws_protocol.JSON.handlers.channels.Net_GetMessageThread_Handler; +import server.logic.ws_protocol.JSON.handlers.channels.Net_ListSubscriptionsFeed_Handler; +import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Request; +import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageThread_Request; +import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request; // --- NEW: Ping --- import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler; @@ -85,6 +91,9 @@ public final class JsonHandlerRegistry { // --- connections --- Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()), + Map.entry("ListSubscriptionsFeed", new Net_ListSubscriptionsFeed_Handler()), + Map.entry("GetChannelMessages", new Net_GetChannelMessages_Handler()), + Map.entry("GetMessageThread", new Net_GetMessageThread_Handler()), // --- system --- Map.entry("Ping", new Net_Ping_Handler()), @@ -119,6 +128,9 @@ public final class JsonHandlerRegistry { // --- connections --- Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class), + Map.entry("ListSubscriptionsFeed", Net_ListSubscriptionsFeed_Request.class), + Map.entry("GetChannelMessages", Net_GetChannelMessages_Request.class), + Map.entry("GetMessageThread", Net_GetMessageThread_Request.class), // --- system --- Map.entry("Ping", Net_Ping_Request.class), diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java index ffc1d5c..92206d1 100644 --- a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/blockchain/Net_AddBlock_Handler.java @@ -5,6 +5,7 @@ import blockchain.BchCryptoVerifier; import blockchain.MsgSubType; import blockchain.body.BodyHasLine; import blockchain.body.BodyHasTarget; +import blockchain.body.CreateChannelBody; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import server.logic.ws_protocol.Base64Ws; @@ -25,6 +26,9 @@ import shine.db.entities.BlockEntry; import utils.blockchain.BlockchainNameUtil; import java.util.Arrays; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.util.concurrent.locks.ReentrantLock; /** @@ -128,6 +132,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { case "prev_line_block_not_found" -> "Не найден блок prevLineNumber для проверки линии"; case "bad_prev_line_hash" -> "Некорректный prevLineHash"; case "db_error_prev_line_check" -> "Ошибка БД при проверке prevLine"; + case "channel_name_already_exists" -> "Канал с таким именем уже существует"; case "internal_error" -> "Внутренняя ошибка сервера при записи блока"; default -> "Ошибка: " + code; }; @@ -228,6 +233,18 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex); } + if (block.body instanceof CreateChannelBody createChannelBody) { + try { + if (channelNameExists(blockchainName, createChannelBody.channelName)) { + return new AddBlockResult(409, "channel_name_already_exists", serverLastNum, serverLastHashHex); + } + } catch (Exception e) { + log.error("AddBlock: channel_name_check_failed (blockchainName={}, channelName={})", + blockchainName, createChannelBody.channelName, e); + return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex); + } + } + // 4.2) запрет дырок: blockNumber строго last+1 int expectedBlockNumber = serverLastNum + 1; if (block.blockNumber != expectedBlockNumber) { @@ -378,6 +395,32 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler { return Base64Ws.decode(b64); } + private boolean channelNameExists(String blockchainName, String channelName) throws Exception { + String sql = """ + SELECT block_bytes + FROM blocks + WHERE bch_name = ? AND msg_type = 0 AND msg_sub_type = 1 + """; + try (Connection c = shine.db.SqliteDbController.getInstance().getConnection(); + PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, blockchainName); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + byte[] bytes = rs.getBytes("block_bytes"); + try { + BchBlockEntry entry = new BchBlockEntry(bytes); + if (entry.body instanceof CreateChannelBody ccb) { + if (ccb.channelName.equalsIgnoreCase(channelName)) return true; + } + } catch (Exception ignored) { + // ignore bad historic rows, uniqueness check is best effort + } + } + } + } + return false; + } + private static long safeAdd(long a, long b) { long r = a + b; if (((a ^ r) & (b ^ r)) < 0) throw new ArithmeticException("long overflow"); diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java new file mode 100644 index 0000000..6d266bc --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/ChannelsReadSupport.java @@ -0,0 +1,240 @@ +package server.logic.ws_protocol.JSON.handlers.channels; + +import blockchain.BchBlockEntry; +import blockchain.body.BodyRecord; +import blockchain.body.CreateChannelBody; +import blockchain.body.TextBody; +import shine.db.MsgSubType; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +final class ChannelsReadSupport { + static final int MSG_TYPE_TEXT = 1; + static final int MSG_TYPE_TECH = 0; + + private ChannelsReadSupport() {} + + static String canonicalLogin(Connection c, String anyCaseLogin) throws SQLException { + String sql = "SELECT login FROM solana_users WHERE login = ? COLLATE NOCASE LIMIT 1"; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, anyCaseLogin); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getString("login") : null; + } + } + } + + static String detectChannelName(Connection c, String ownerBch, int rootNumber) throws SQLException { + if (rootNumber == 0) return "0"; + + String sql = "SELECT block_bytes FROM blocks WHERE bch_name=? AND block_number=? LIMIT 1"; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, ownerBch); + ps.setInt(2, rootNumber); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return null; + byte[] bytes = rs.getBytes("block_bytes"); + BchBlockEntry e = new BchBlockEntry(bytes); + BodyRecord body = e.body; + if (body instanceof CreateChannelBody ccb) return ccb.channelName; + return null; + } catch (Exception ignored) { + return null; + } + } + } + + static int countPosts(Connection c, String ownerBch, int lineCode) throws SQLException { + String sql = "SELECT COUNT(*) AS cnt FROM blocks WHERE bch_name=? AND msg_type=? AND msg_sub_type=? AND line_code=?"; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, ownerBch); + ps.setInt(2, MSG_TYPE_TEXT); + ps.setInt(3, MsgSubType.TEXT_POST); + ps.setInt(4, lineCode); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? rs.getInt("cnt") : 0; + } + } + } + + static PostBlock loadLastPost(Connection c, String ownerBch, int lineCode) throws SQLException { + String sql = """ + SELECT login,bch_name,block_number,block_hash,block_bytes + FROM blocks + WHERE bch_name=? AND msg_type=? AND msg_sub_type=? AND line_code=? + ORDER BY block_number DESC + LIMIT 1 + """; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, ownerBch); + ps.setInt(2, MSG_TYPE_TEXT); + ps.setInt(3, MsgSubType.TEXT_POST); + ps.setInt(4, lineCode); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return null; + PostBlock pb = new PostBlock(); + pb.login = rs.getString("login"); + pb.bchName = rs.getString("bch_name"); + pb.blockNumber = rs.getInt("block_number"); + pb.blockHash = rs.getBytes("block_hash"); + pb.blockBytes = rs.getBytes("block_bytes"); + return pb; + } + } + } + + static PostBlock loadLastVersion(Connection c, String ownerBch, int originalBlockNumber, byte[] originalHash) throws SQLException { + String sql = """ + SELECT login,bch_name,block_number,block_hash,block_bytes + FROM blocks + WHERE bch_name=? AND msg_type=? AND msg_sub_type=? + AND to_bch_name=? AND to_block_number=? AND to_block_hash=? + ORDER BY block_number DESC + LIMIT 1 + """; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, ownerBch); + ps.setInt(2, MSG_TYPE_TEXT); + ps.setInt(3, MsgSubType.TEXT_EDIT_POST); + ps.setString(4, ownerBch); + ps.setInt(5, originalBlockNumber); + ps.setBytes(6, originalHash); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return null; + PostBlock pb = new PostBlock(); + pb.login = rs.getString("login"); + pb.bchName = rs.getString("bch_name"); + pb.blockNumber = rs.getInt("block_number"); + pb.blockHash = rs.getBytes("block_hash"); + pb.blockBytes = rs.getBytes("block_bytes"); + return pb; + } + } + } + + static TextInfo parseTextAndTime(byte[] blockBytes) { + try { + BchBlockEntry e = new BchBlockEntry(blockBytes); + TextInfo ti = new TextInfo(); + ti.createdAtMs = e.timeMs; + if (e.body instanceof TextBody tb) { + ti.text = tb.message; + } + return ti; + } catch (Exception ex) { + return new TextInfo(); + } + } + + static List channelPosts(Connection c, String ownerBch, int lineCode, int limit, boolean asc) throws SQLException { + String order = asc ? "ASC" : "DESC"; + String sql = """ + SELECT login,bch_name,block_number,block_hash,block_bytes + FROM blocks + WHERE bch_name=? AND msg_type=? AND msg_sub_type=? AND line_code=? + ORDER BY block_number """ + order + " LIMIT ?"; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, ownerBch); + ps.setInt(2, MSG_TYPE_TEXT); + ps.setInt(3, MsgSubType.TEXT_POST); + ps.setInt(4, lineCode); + ps.setInt(5, limit); + try (ResultSet rs = ps.executeQuery()) { + List out = new ArrayList<>(); + while (rs.next()) { + PostBlock pb = new PostBlock(); + pb.login = rs.getString("login"); + pb.bchName = rs.getString("bch_name"); + pb.blockNumber = rs.getInt("block_number"); + pb.blockHash = rs.getBytes("block_hash"); + pb.blockBytes = rs.getBytes("block_bytes"); + out.add(pb); + } + return out; + } + } + } + + static List versionsForPost(Connection c, String ownerBch, int originalBlock, byte[] originalHash) throws SQLException { + String sql = """ + SELECT login,bch_name,block_number,block_hash,block_bytes + FROM blocks + WHERE bch_name=? AND msg_type=? AND msg_sub_type=? + AND to_bch_name=? AND to_block_number=? AND to_block_hash=? + ORDER BY block_number ASC + """; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, ownerBch); + ps.setInt(2, MSG_TYPE_TEXT); + ps.setInt(3, MsgSubType.TEXT_EDIT_POST); + ps.setString(4, ownerBch); + ps.setInt(5, originalBlock); + ps.setBytes(6, originalHash); + try (ResultSet rs = ps.executeQuery()) { + List out = new ArrayList<>(); + while (rs.next()) { + PostBlock pb = new PostBlock(); + pb.login = rs.getString("login"); + pb.bchName = rs.getString("bch_name"); + pb.blockNumber = rs.getInt("block_number"); + pb.blockHash = rs.getBytes("block_hash"); + pb.blockBytes = rs.getBytes("block_bytes"); + out.add(pb); + } + return out; + } + } + } + + static int[] loadStats(Connection c, String bch, int blockNumber, byte[] blockHash) throws SQLException { + String sql = "SELECT likes_count,replies_count FROM message_stats WHERE to_bch_name=? AND to_block_number=? AND to_block_hash=? LIMIT 1"; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, bch); + ps.setInt(2, blockNumber); + ps.setBytes(3, blockHash); + try (ResultSet rs = ps.executeQuery()) { + if (!rs.next()) return new int[] {0, 0}; + return new int[] {rs.getInt("likes_count"), rs.getInt("replies_count")}; + } + } + } + + static byte[] hexToBytes(String s) { + if (s == null) return null; + String x = s.trim(); + if ((x.length() & 1) != 0) throw new IllegalArgumentException("hex length must be even"); + byte[] out = new byte[x.length() / 2]; + for (int i = 0; i < out.length; i++) { + int hi = Character.digit(x.charAt(i * 2), 16); + int lo = Character.digit(x.charAt(i * 2 + 1), 16); + if (hi < 0 || lo < 0) throw new IllegalArgumentException("bad hex"); + out[i] = (byte) ((hi << 4) | lo); + } + return out; + } + + static String toHex(byte[] bytes) { + if (bytes == null) return null; + StringBuilder sb = new StringBuilder(bytes.length * 2); + for (byte b : bytes) sb.append(String.format("%02x", b)); + return sb.toString(); + } + + static final class PostBlock { + String login; + String bchName; + int blockNumber; + byte[] blockHash; + byte[] blockBytes; + } + + static final class TextInfo { + String text = ""; + long createdAtMs = 0L; + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java new file mode 100644 index 0000000..32aeace --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetChannelMessages_Handler.java @@ -0,0 +1,115 @@ +package server.logic.ws_protocol.JSON.handlers.channels; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import server.logic.ws_protocol.JSON.ConnectionContext; +import server.logic.ws_protocol.JSON.entyties.Net_Request; +import server.logic.ws_protocol.JSON.entyties.Net_Response; +import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; +import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Request; +import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Response; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; +import server.logic.ws_protocol.WireCodes; +import shine.db.SqliteDbController; + +import java.sql.Connection; +import java.util.ArrayList; +import java.util.List; + +public class Net_GetChannelMessages_Handler implements JsonMessageHandler { + private static final Logger log = LoggerFactory.getLogger(Net_GetChannelMessages_Handler.class); + + @Override + public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) { + Net_GetChannelMessages_Request req = (Net_GetChannelMessages_Request) baseRequest; + if (req.getChannel() == null + || req.getChannel().getOwnerBlockchainName() == null + || req.getChannel().getOwnerBlockchainName().isBlank() + || req.getChannel().getChannelRootBlockNumber() == null) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля channel"); + } + + int limit = req.getLimit() == null ? 30 : req.getLimit(); + if (limit <= 0 || limit > 1000) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "limit_too_large", "Некорректный limit"); + } + + boolean asc = req.getSort() == null || !"desc".equalsIgnoreCase(req.getSort()); + + try (Connection c = SqliteDbController.getInstance().getConnection()) { + String ownerBch = req.getChannel().getOwnerBlockchainName(); + int lineCode = req.getChannel().getChannelRootBlockNumber(); + + Net_GetChannelMessages_Response resp = new Net_GetChannelMessages_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + + Net_GetChannelMessages_Response.Channel channel = new Net_GetChannelMessages_Response.Channel(); + channel.setOwnerBlockchainName(ownerBch); + channel.setOwnerLogin(ownerBch.contains("-") ? ownerBch.substring(0, ownerBch.indexOf('-')) : ownerBch); + channel.setChannelName(ChannelsReadSupport.detectChannelName(c, ownerBch, lineCode)); + Net_GetChannelMessages_Response.BlockRef rootRef = new Net_GetChannelMessages_Response.BlockRef(); + rootRef.setBlockNumber(lineCode); + rootRef.setBlockHash(req.getChannel().getChannelRootBlockHash()); + channel.setChannelRoot(rootRef); + resp.setChannel(channel); + + List posts = ChannelsReadSupport.channelPosts(c, ownerBch, lineCode, limit, asc); + List items = new ArrayList<>(); + + for (ChannelsReadSupport.PostBlock post : posts) { + Net_GetChannelMessages_Response.MessageItem item = new Net_GetChannelMessages_Response.MessageItem(); + Net_GetChannelMessages_Response.BlockRef msgRef = new Net_GetChannelMessages_Response.BlockRef(); + msgRef.setBlockNumber(post.blockNumber); + msgRef.setBlockHash(ChannelsReadSupport.toHex(post.blockHash)); + item.setMessageRef(msgRef); + item.setAuthorLogin(post.login); + item.setAuthorBlockchainName(post.bchName); + + List versionsOut = new ArrayList<>(); + int index = 1; + + ChannelsReadSupport.TextInfo postText = ChannelsReadSupport.parseTextAndTime(post.blockBytes); + Net_GetChannelMessages_Response.VersionItem v1 = new Net_GetChannelMessages_Response.VersionItem(); + v1.setVersionIndex(index++); + v1.setBlockNumber(post.blockNumber); + v1.setBlockHash(ChannelsReadSupport.toHex(post.blockHash)); + v1.setText(postText.text); + v1.setCreatedAtMs(postText.createdAtMs); + versionsOut.add(v1); + + List edits = ChannelsReadSupport.versionsForPost(c, ownerBch, post.blockNumber, post.blockHash); + for (ChannelsReadSupport.PostBlock edit : edits) { + ChannelsReadSupport.TextInfo editText = ChannelsReadSupport.parseTextAndTime(edit.blockBytes); + Net_GetChannelMessages_Response.VersionItem ve = new Net_GetChannelMessages_Response.VersionItem(); + ve.setVersionIndex(index++); + ve.setBlockNumber(edit.blockNumber); + ve.setBlockHash(ChannelsReadSupport.toHex(edit.blockHash)); + ve.setText(editText.text); + ve.setCreatedAtMs(editText.createdAtMs); + versionsOut.add(ve); + } + + item.setVersions(versionsOut); + item.setVersionsTotal(versionsOut.size()); + + Net_GetChannelMessages_Response.VersionItem lastV = versionsOut.get(versionsOut.size() - 1); + item.setText(lastV.getText()); + item.setCreatedAtMs(postText.createdAtMs); + + int[] stats = ChannelsReadSupport.loadStats(c, ownerBch, post.blockNumber, post.blockHash); + item.setLikesCount(stats[0]); + item.setRepliesCount(stats[1]); + + items.add(item); + } + + resp.setMessages(items); + return resp; + } catch (Exception e) { + log.error("GetChannelMessages failed", e); + return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера"); + } + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetMessageThread_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetMessageThread_Handler.java new file mode 100644 index 0000000..be93672 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_GetMessageThread_Handler.java @@ -0,0 +1,224 @@ +package server.logic.ws_protocol.JSON.handlers.channels; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import server.logic.ws_protocol.JSON.ConnectionContext; +import server.logic.ws_protocol.JSON.entyties.Net_Request; +import server.logic.ws_protocol.JSON.entyties.Net_Response; +import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; +import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Response; +import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageThread_Request; +import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageThread_Response; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; +import server.logic.ws_protocol.WireCodes; +import shine.db.MsgSubType; +import shine.db.SqliteDbController; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; + +public class Net_GetMessageThread_Handler implements JsonMessageHandler { + private static final Logger log = LoggerFactory.getLogger(Net_GetMessageThread_Handler.class); + + @Override + public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) { + Net_GetMessageThread_Request req = (Net_GetMessageThread_Request) baseRequest; + if (req.getMessage() == null || req.getMessage().getBlockchainName() == null || req.getMessage().getBlockNumber() == null) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля message"); + } + + int depthUp = req.getDepthUp() == null ? 20 : Math.max(0, req.getDepthUp()); + int depthDown = req.getDepthDown() == null ? 2 : Math.max(0, req.getDepthDown()); + int childLimit = req.getLimitChildrenPerNode() == null ? 50 : Math.max(1, req.getLimitChildrenPerNode()); + + try (Connection c = SqliteDbController.getInstance().getConnection()) { + PostRow focusRow = findByNumber(c, req.getMessage().getBlockchainName(), req.getMessage().getBlockNumber()); + if (focusRow == null) { + return NetExceptionResponseFactory.error(req, 404, "message_not_found", "Сообщение не найдено"); + } + + Net_GetMessageThread_Response resp = new Net_GetMessageThread_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + + resp.setFocus(toNode(c, focusRow)); + + List ancestors = new ArrayList<>(); + PostRow cur = focusRow; + for (int i = 0; i < depthUp; i++) { + if (cur.toBlockNumber == null || cur.toBchName == null) break; + PostRow parent = findByNumber(c, cur.toBchName, cur.toBlockNumber); + if (parent == null) break; + ancestors.add(0, toNode(c, parent)); + cur = parent; + } + resp.setAncestors(ancestors); + + resp.setDescendants(loadChildren(c, focusRow, depthDown, childLimit)); + return resp; + } catch (Exception e) { + log.error("GetMessageThread failed", e); + return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера"); + } + } + + private List loadChildren(Connection c, PostRow parent, int depthDown, int childLimit) throws Exception { + if (depthDown <= 0) return List.of(); + List replies = findReplies(c, parent.bchName, parent.blockNumber, parent.blockHash, childLimit); + List out = new ArrayList<>(); + for (PostRow row : replies) { + Net_GetMessageThread_Response.MessageNodeTree t = new Net_GetMessageThread_Response.MessageNodeTree(); + t.setNode(toNode(c, row)); + t.setChildren(loadChildren(c, row, depthDown - 1, childLimit)); + out.add(t); + } + return out; + } + + private List findReplies(Connection c, String toBchName, int toBlockNumber, byte[] toBlockHash, int limit) throws Exception { + String sql = """ + SELECT login,bch_name,block_number,block_hash,block_bytes,to_bch_name,to_block_number,to_block_hash,line_code,msg_sub_type + FROM blocks + WHERE msg_type=1 AND msg_sub_type=? + AND to_bch_name=? AND to_block_number=? AND to_block_hash=? + ORDER BY block_number ASC + LIMIT ? + """; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setInt(1, MsgSubType.TEXT_REPLY); + ps.setString(2, toBchName); + ps.setInt(3, toBlockNumber); + ps.setBytes(4, toBlockHash); + ps.setInt(5, limit); + try (ResultSet rs = ps.executeQuery()) { + List out = new ArrayList<>(); + while (rs.next()) out.add(mapRow(rs)); + return out; + } + } + } + + private PostRow findByNumber(Connection c, String bchName, int blockNumber) throws Exception { + String sql = """ + SELECT login,bch_name,block_number,block_hash,block_bytes,to_bch_name,to_block_number,to_block_hash,line_code,msg_sub_type + FROM blocks + WHERE bch_name=? AND block_number=? + LIMIT 1 + """; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, bchName); + ps.setInt(2, blockNumber); + try (ResultSet rs = ps.executeQuery()) { + return rs.next() ? mapRow(rs) : null; + } + } + } + + private PostRow mapRow(ResultSet rs) throws Exception { + PostRow row = new PostRow(); + row.login = rs.getString("login"); + row.bchName = rs.getString("bch_name"); + row.blockNumber = rs.getInt("block_number"); + row.blockHash = rs.getBytes("block_hash"); + row.blockBytes = rs.getBytes("block_bytes"); + row.toBchName = rs.getString("to_bch_name"); + row.toBlockNumber = (Integer) rs.getObject("to_block_number"); + row.toBlockHash = rs.getBytes("to_block_hash"); + row.lineCode = (Integer) rs.getObject("line_code"); + row.msgSubType = rs.getInt("msg_sub_type"); + return row; + } + + private Net_GetMessageThread_Response.MessageNode toNode(Connection c, PostRow row) throws Exception { + Net_GetMessageThread_Response.MessageNode node = new Net_GetMessageThread_Response.MessageNode(); + Net_GetChannelMessages_Response.BlockRef ref = new Net_GetChannelMessages_Response.BlockRef(); + ref.setBlockNumber(row.blockNumber); + ref.setBlockHash(ChannelsReadSupport.toHex(row.blockHash)); + node.setMessageRef(ref); + node.setAuthorLogin(row.login); + node.setAuthorBlockchainName(row.bchName); + + ChannelsReadSupport.TextInfo base = ChannelsReadSupport.parseTextAndTime(row.blockBytes); + node.setCreatedAtMs(base.createdAtMs); + + List versions = new ArrayList<>(); + Net_GetChannelMessages_Response.VersionItem first = new Net_GetChannelMessages_Response.VersionItem(); + first.setVersionIndex(1); + first.setBlockNumber(row.blockNumber); + first.setBlockHash(ChannelsReadSupport.toHex(row.blockHash)); + first.setText(base.text); + first.setCreatedAtMs(base.createdAtMs); + versions.add(first); + + short editType = row.msgSubType == MsgSubType.TEXT_REPLY ? MsgSubType.TEXT_EDIT_REPLY : MsgSubType.TEXT_EDIT_POST; + for (PostRow edit : findEdits(c, row.bchName, row.blockNumber, row.blockHash, editType)) { + ChannelsReadSupport.TextInfo et = ChannelsReadSupport.parseTextAndTime(edit.blockBytes); + Net_GetChannelMessages_Response.VersionItem v = new Net_GetChannelMessages_Response.VersionItem(); + v.setVersionIndex(versions.size() + 1); + v.setBlockNumber(edit.blockNumber); + v.setBlockHash(ChannelsReadSupport.toHex(edit.blockHash)); + v.setText(et.text); + v.setCreatedAtMs(et.createdAtMs); + versions.add(v); + } + + node.setVersions(versions); + node.setVersionsTotal(versions.size()); + node.setText(versions.get(versions.size() - 1).getText()); + + int[] stats = ChannelsReadSupport.loadStats(c, row.bchName, row.blockNumber, row.blockHash); + node.setLikesCount(stats[0]); + node.setRepliesCount(stats[1]); + + if (row.lineCode != null && row.lineCode >= 0) { + Net_GetMessageThread_Response.ChannelInfo ci = new Net_GetMessageThread_Response.ChannelInfo(); + ci.setOwnerBlockchainName(row.bchName); + Net_GetChannelMessages_Response.BlockRef root = new Net_GetChannelMessages_Response.BlockRef(); + root.setBlockNumber(row.lineCode); + root.setBlockHash(null); + ci.setChannelRoot(root); + node.setChannelInfo(ci); + } + + return node; + } + + private List findEdits(Connection c, String bch, int targetBlock, byte[] targetHash, int subType) throws Exception { + String sql = """ + SELECT login,bch_name,block_number,block_hash,block_bytes,to_bch_name,to_block_number,to_block_hash,line_code,msg_sub_type + FROM blocks + WHERE bch_name=? AND msg_type=1 AND msg_sub_type=? + AND to_bch_name=? AND to_block_number=? AND to_block_hash=? + ORDER BY block_number ASC + """; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, bch); + ps.setInt(2, subType); + ps.setString(3, bch); + ps.setInt(4, targetBlock); + ps.setBytes(5, targetHash); + try (ResultSet rs = ps.executeQuery()) { + List out = new ArrayList<>(); + while (rs.next()) out.add(mapRow(rs)); + return out; + } + } + } + + private static final class PostRow { + String login; + String bchName; + int blockNumber; + byte[] blockHash; + byte[] blockBytes; + String toBchName; + Integer toBlockNumber; + byte[] toBlockHash; + Integer lineCode; + int msgSubType; + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_ListSubscriptionsFeed_Handler.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_ListSubscriptionsFeed_Handler.java new file mode 100644 index 0000000..575e0c8 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/Net_ListSubscriptionsFeed_Handler.java @@ -0,0 +1,168 @@ +package server.logic.ws_protocol.JSON.handlers.channels; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import server.logic.ws_protocol.JSON.ConnectionContext; +import server.logic.ws_protocol.JSON.entyties.Net_Request; +import server.logic.ws_protocol.JSON.entyties.Net_Response; +import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler; +import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request; +import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Response; +import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory; +import server.logic.ws_protocol.WireCodes; +import shine.db.MsgSubType; +import shine.db.SqliteDbController; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.util.ArrayList; +import java.util.List; + +public class Net_ListSubscriptionsFeed_Handler implements JsonMessageHandler { + private static final Logger log = LoggerFactory.getLogger(Net_ListSubscriptionsFeed_Handler.class); + + @Override + public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) { + Net_ListSubscriptionsFeed_Request req = (Net_ListSubscriptionsFeed_Request) baseRequest; + if (req.getLogin() == null || req.getLogin().isBlank()) { + return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login"); + } + + try (Connection c = SqliteDbController.getInstance().getConnection()) { + String canonicalLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin().trim()); + if (canonicalLogin == null) { + return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден"); + } + + Net_ListSubscriptionsFeed_Response resp = new Net_ListSubscriptionsFeed_Response(); + resp.setOp(req.getOp()); + resp.setRequestId(req.getRequestId()); + resp.setStatus(WireCodes.Status.OK); + resp.setLogin(canonicalLogin); + + List own = loadOwnChannels(c, canonicalLogin); + List followedUsersChannels = loadFollowedChannels(c, canonicalLogin, true); + List followedChannels = loadFollowedChannels(c, canonicalLogin, false); + + resp.setOwnedChannels(buildSummaries(c, own)); + resp.setFollowedUsersChannels(buildSummaries(c, followedUsersChannels)); + resp.setFollowedChannels(buildSummaries(c, followedChannels)); + + return resp; + } catch (Exception e) { + log.error("ListSubscriptionsFeed failed", e); + return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера"); + } + } + + private List buildSummaries(Connection c, List keys) throws Exception { + List out = new ArrayList<>(); + for (ChannelKey key : keys) { + Net_ListSubscriptionsFeed_Response.ChannelSummary row = new Net_ListSubscriptionsFeed_Response.ChannelSummary(); + Net_ListSubscriptionsFeed_Response.ChannelRef channelRef = new Net_ListSubscriptionsFeed_Response.ChannelRef(); + channelRef.setOwnerLogin(key.ownerLogin); + channelRef.setOwnerBlockchainName(key.ownerBch); + channelRef.setChannelName(ChannelsReadSupport.detectChannelName(c, key.ownerBch, key.rootNumber)); + channelRef.setPersonal(key.rootNumber == 0); + + Net_ListSubscriptionsFeed_Response.BlockRef rootRef = new Net_ListSubscriptionsFeed_Response.BlockRef(); + rootRef.setBlockNumber(key.rootNumber); + rootRef.setBlockHash(ChannelsReadSupport.toHex(key.rootHash)); + channelRef.setChannelRoot(rootRef); + + row.setChannel(channelRef); + row.setMessagesCount(ChannelsReadSupport.countPosts(c, key.ownerBch, key.rootNumber)); + + ChannelsReadSupport.PostBlock lastPost = ChannelsReadSupport.loadLastPost(c, key.ownerBch, key.rootNumber); + if (lastPost != null) { + ChannelsReadSupport.PostBlock actual = ChannelsReadSupport.loadLastVersion(c, key.ownerBch, lastPost.blockNumber, lastPost.blockHash); + if (actual == null) actual = lastPost; + + ChannelsReadSupport.TextInfo textInfo = ChannelsReadSupport.parseTextAndTime(actual.blockBytes); + Net_ListSubscriptionsFeed_Response.LastMessage lm = new Net_ListSubscriptionsFeed_Response.LastMessage(); + Net_ListSubscriptionsFeed_Response.BlockRef msgRef = new Net_ListSubscriptionsFeed_Response.BlockRef(); + msgRef.setBlockNumber(actual.blockNumber); + msgRef.setBlockHash(ChannelsReadSupport.toHex(actual.blockHash)); + lm.setMessageRef(msgRef); + lm.setText(textInfo.text); + lm.setCreatedAtMs(textInfo.createdAtMs); + lm.setAuthorLogin(actual.login); + lm.setAuthorBlockchainName(actual.bchName); + row.setLastMessage(lm); + } + + out.add(row); + } + return out; + } + + private List loadOwnChannels(Connection c, String canonicalLogin) throws Exception { + List out = new ArrayList<>(); + String bchSql = "SELECT blockchain_name FROM blockchain_state WHERE login=? ORDER BY blockchain_name"; + try (PreparedStatement bchPs = c.prepareStatement(bchSql)) { + bchPs.setString(1, canonicalLogin); + try (ResultSet bchRs = bchPs.executeQuery()) { + while (bchRs.next()) { + String bch = bchRs.getString("blockchain_name"); + out.add(new ChannelKey(canonicalLogin, bch, 0, new byte[32])); + + String chSql = "SELECT block_number,block_hash FROM blocks WHERE bch_name=? AND msg_type=? AND msg_sub_type=? ORDER BY block_number"; + try (PreparedStatement chPs = c.prepareStatement(chSql)) { + chPs.setString(1, bch); + chPs.setInt(2, ChannelsReadSupport.MSG_TYPE_TECH); + chPs.setInt(3, 1); + try (ResultSet chRs = chPs.executeQuery()) { + while (chRs.next()) { + out.add(new ChannelKey(canonicalLogin, bch, chRs.getInt("block_number"), chRs.getBytes("block_hash"))); + } + } + } + } + } + } + return out; + } + + private List loadFollowedChannels(Connection c, String canonicalLogin, boolean onlyUserRoots) throws Exception { + List out = new ArrayList<>(); + String sql = """ + SELECT cs.to_login, cs.to_bch_name, COALESCE(cs.to_block_number,0) AS root_number, cs.to_block_hash + FROM connections_state cs + WHERE cs.login=? AND cs.rel_type=? + ORDER BY cs.to_login, cs.to_bch_name, root_number + """; + try (PreparedStatement ps = c.prepareStatement(sql)) { + ps.setString(1, canonicalLogin); + ps.setInt(2, MsgSubType.CONNECTION_FOLLOW); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + int rootNumber = rs.getInt("root_number"); + if (onlyUserRoots && rootNumber != 0) continue; + if (!onlyUserRoots && rootNumber == 0) continue; + out.add(new ChannelKey( + rs.getString("to_login"), + rs.getString("to_bch_name"), + rootNumber, + rs.getBytes("to_block_hash") + )); + } + } + } + return out; + } + + private static final class ChannelKey { + final String ownerLogin; + final String ownerBch; + final int rootNumber; + final byte[] rootHash; + + private ChannelKey(String ownerLogin, String ownerBch, int rootNumber, byte[] rootHash) { + this.ownerLogin = ownerLogin; + this.ownerBch = ownerBch; + this.rootNumber = rootNumber; + this.rootHash = rootHash; + } + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Request.java new file mode 100644 index 0000000..c18363e --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Request.java @@ -0,0 +1,33 @@ +package server.logic.ws_protocol.JSON.handlers.channels.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +public class Net_GetChannelMessages_Request extends Net_Request { + private ChannelSelector channel; + private Integer limit; + private String sort; + + public ChannelSelector getChannel() { return channel; } + public void setChannel(ChannelSelector channel) { this.channel = channel; } + + public Integer getLimit() { return limit; } + public void setLimit(Integer limit) { this.limit = limit; } + + public String getSort() { return sort; } + public void setSort(String sort) { this.sort = sort; } + + public static class ChannelSelector { + private String ownerBlockchainName; + private Integer channelRootBlockNumber; + private String channelRootBlockHash; + + public String getOwnerBlockchainName() { return ownerBlockchainName; } + public void setOwnerBlockchainName(String ownerBlockchainName) { this.ownerBlockchainName = ownerBlockchainName; } + + public Integer getChannelRootBlockNumber() { return channelRootBlockNumber; } + public void setChannelRootBlockNumber(Integer channelRootBlockNumber) { this.channelRootBlockNumber = channelRootBlockNumber; } + + public String getChannelRootBlockHash() { return channelRootBlockHash; } + public void setChannelRootBlockHash(String channelRootBlockHash) { this.channelRootBlockHash = channelRootBlockHash; } + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Response.java new file mode 100644 index 0000000..cc6fd93 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetChannelMessages_Response.java @@ -0,0 +1,109 @@ +package server.logic.ws_protocol.JSON.handlers.channels.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +import java.util.ArrayList; +import java.util.List; + +public class Net_GetChannelMessages_Response extends Net_Response { + private Channel channel; + private List messages = new ArrayList<>(); + + public Channel getChannel() { return channel; } + public void setChannel(Channel channel) { this.channel = channel; } + + public List getMessages() { return messages; } + public void setMessages(List messages) { this.messages = messages; } + + public static class Channel { + private String ownerLogin; + private String ownerBlockchainName; + private String channelName; + private BlockRef channelRoot; + + public String getOwnerLogin() { return ownerLogin; } + public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; } + + public String getOwnerBlockchainName() { return ownerBlockchainName; } + public void setOwnerBlockchainName(String ownerBlockchainName) { this.ownerBlockchainName = ownerBlockchainName; } + + public String getChannelName() { return channelName; } + public void setChannelName(String channelName) { this.channelName = channelName; } + + public BlockRef getChannelRoot() { return channelRoot; } + public void setChannelRoot(BlockRef channelRoot) { this.channelRoot = channelRoot; } + } + + public static class MessageItem { + private BlockRef messageRef; + private String authorLogin; + private String authorBlockchainName; + private long createdAtMs; + private String text; + private int likesCount; + private int repliesCount; + private int versionsTotal; + private List versions = new ArrayList<>(); + + public BlockRef getMessageRef() { return messageRef; } + public void setMessageRef(BlockRef messageRef) { this.messageRef = messageRef; } + + public String getAuthorLogin() { return authorLogin; } + public void setAuthorLogin(String authorLogin) { this.authorLogin = authorLogin; } + + public String getAuthorBlockchainName() { return authorBlockchainName; } + public void setAuthorBlockchainName(String authorBlockchainName) { this.authorBlockchainName = authorBlockchainName; } + + public long getCreatedAtMs() { return createdAtMs; } + public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; } + + public String getText() { return text; } + public void setText(String text) { this.text = text; } + + public int getLikesCount() { return likesCount; } + public void setLikesCount(int likesCount) { this.likesCount = likesCount; } + + public int getRepliesCount() { return repliesCount; } + public void setRepliesCount(int repliesCount) { this.repliesCount = repliesCount; } + + public int getVersionsTotal() { return versionsTotal; } + public void setVersionsTotal(int versionsTotal) { this.versionsTotal = versionsTotal; } + + public List getVersions() { return versions; } + public void setVersions(List versions) { this.versions = versions; } + } + + public static class VersionItem { + private int versionIndex; + private int blockNumber; + private String blockHash; + private String text; + private long createdAtMs; + + public int getVersionIndex() { return versionIndex; } + public void setVersionIndex(int versionIndex) { this.versionIndex = versionIndex; } + + public int getBlockNumber() { return blockNumber; } + public void setBlockNumber(int blockNumber) { this.blockNumber = blockNumber; } + + public String getBlockHash() { return blockHash; } + public void setBlockHash(String blockHash) { this.blockHash = blockHash; } + + public String getText() { return text; } + public void setText(String text) { this.text = text; } + + public long getCreatedAtMs() { return createdAtMs; } + public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; } + } + + public static class BlockRef { + private int blockNumber; + private String blockHash; + + public int getBlockNumber() { return blockNumber; } + public void setBlockNumber(int blockNumber) { this.blockNumber = blockNumber; } + + public String getBlockHash() { return blockHash; } + public void setBlockHash(String blockHash) { this.blockHash = blockHash; } + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetMessageThread_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetMessageThread_Request.java new file mode 100644 index 0000000..269df09 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetMessageThread_Request.java @@ -0,0 +1,37 @@ +package server.logic.ws_protocol.JSON.handlers.channels.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +public class Net_GetMessageThread_Request extends Net_Request { + private MessageSelector message; + private Integer depthUp; + private Integer depthDown; + private Integer limitChildrenPerNode; + + public MessageSelector getMessage() { return message; } + public void setMessage(MessageSelector message) { this.message = message; } + + public Integer getDepthUp() { return depthUp; } + public void setDepthUp(Integer depthUp) { this.depthUp = depthUp; } + + public Integer getDepthDown() { return depthDown; } + public void setDepthDown(Integer depthDown) { this.depthDown = depthDown; } + + public Integer getLimitChildrenPerNode() { return limitChildrenPerNode; } + public void setLimitChildrenPerNode(Integer limitChildrenPerNode) { this.limitChildrenPerNode = limitChildrenPerNode; } + + public static class MessageSelector { + private String blockchainName; + private Integer blockNumber; + private String blockHash; + + public String getBlockchainName() { return blockchainName; } + public void setBlockchainName(String blockchainName) { this.blockchainName = blockchainName; } + + public Integer getBlockNumber() { return blockNumber; } + public void setBlockNumber(Integer blockNumber) { this.blockNumber = blockNumber; } + + public String getBlockHash() { return blockHash; } + public void setBlockHash(String blockHash) { this.blockHash = blockHash; } + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetMessageThread_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetMessageThread_Response.java new file mode 100644 index 0000000..963aff1 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_GetMessageThread_Response.java @@ -0,0 +1,50 @@ +package server.logic.ws_protocol.JSON.handlers.channels.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +import java.util.ArrayList; +import java.util.List; + +public class Net_GetMessageThread_Response extends Net_Response { + private List ancestors = new ArrayList<>(); + private MessageNode focus; + private List descendants = new ArrayList<>(); + + public List getAncestors() { return ancestors; } + public void setAncestors(List ancestors) { this.ancestors = ancestors; } + + public MessageNode getFocus() { return focus; } + public void setFocus(MessageNode focus) { this.focus = focus; } + + public List getDescendants() { return descendants; } + public void setDescendants(List descendants) { this.descendants = descendants; } + + public static class MessageNode extends Net_GetChannelMessages_Response.MessageItem { + private ChannelInfo channelInfo; + + public ChannelInfo getChannelInfo() { return channelInfo; } + public void setChannelInfo(ChannelInfo channelInfo) { this.channelInfo = channelInfo; } + } + + public static class ChannelInfo { + private String ownerBlockchainName; + private Net_GetChannelMessages_Response.BlockRef channelRoot; + + public String getOwnerBlockchainName() { return ownerBlockchainName; } + public void setOwnerBlockchainName(String ownerBlockchainName) { this.ownerBlockchainName = ownerBlockchainName; } + + public Net_GetChannelMessages_Response.BlockRef getChannelRoot() { return channelRoot; } + public void setChannelRoot(Net_GetChannelMessages_Response.BlockRef channelRoot) { this.channelRoot = channelRoot; } + } + + public static class MessageNodeTree { + private MessageNode node; + private List children = new ArrayList<>(); + + public MessageNode getNode() { return node; } + public void setNode(MessageNode node) { this.node = node; } + + public List getChildren() { return children; } + public void setChildren(List children) { this.children = children; } + } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Request.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Request.java new file mode 100644 index 0000000..e4230f5 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Request.java @@ -0,0 +1,14 @@ +package server.logic.ws_protocol.JSON.handlers.channels.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Request; + +public class Net_ListSubscriptionsFeed_Request extends Net_Request { + private String login; + private Integer limit; + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } + + public Integer getLimit() { return limit; } + public void setLimit(Integer limit) { this.limit = limit; } +} diff --git a/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Response.java b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Response.java new file mode 100644 index 0000000..4fb61b9 --- /dev/null +++ b/shine-server-net-protocol/src/main/java/server/logic/ws_protocol/JSON/handlers/channels/entyties/Net_ListSubscriptionsFeed_Response.java @@ -0,0 +1,97 @@ +package server.logic.ws_protocol.JSON.handlers.channels.entyties; + +import server.logic.ws_protocol.JSON.entyties.Net_Response; + +import java.util.ArrayList; +import java.util.List; + +public class Net_ListSubscriptionsFeed_Response extends Net_Response { + private String login; + private List ownedChannels = new ArrayList<>(); + private List followedUsersChannels = new ArrayList<>(); + private List followedChannels = new ArrayList<>(); + + public String getLogin() { return login; } + public void setLogin(String login) { this.login = login; } + + public List getOwnedChannels() { return ownedChannels; } + public void setOwnedChannels(List ownedChannels) { this.ownedChannels = ownedChannels; } + + public List getFollowedUsersChannels() { return followedUsersChannels; } + public void setFollowedUsersChannels(List followedUsersChannels) { this.followedUsersChannels = followedUsersChannels; } + + public List getFollowedChannels() { return followedChannels; } + public void setFollowedChannels(List followedChannels) { this.followedChannels = followedChannels; } + + public static class ChannelSummary { + private ChannelRef channel; + private int messagesCount; + private LastMessage lastMessage; + + public ChannelRef getChannel() { return channel; } + public void setChannel(ChannelRef channel) { this.channel = channel; } + + public int getMessagesCount() { return messagesCount; } + public void setMessagesCount(int messagesCount) { this.messagesCount = messagesCount; } + + public LastMessage getLastMessage() { return lastMessage; } + public void setLastMessage(LastMessage lastMessage) { this.lastMessage = lastMessage; } + } + + public static class ChannelRef { + private String ownerLogin; + private String ownerBlockchainName; + private String channelName; + private boolean personal; + private BlockRef channelRoot; + + public String getOwnerLogin() { return ownerLogin; } + public void setOwnerLogin(String ownerLogin) { this.ownerLogin = ownerLogin; } + + public String getOwnerBlockchainName() { return ownerBlockchainName; } + public void setOwnerBlockchainName(String ownerBlockchainName) { this.ownerBlockchainName = ownerBlockchainName; } + + public String getChannelName() { return channelName; } + public void setChannelName(String channelName) { this.channelName = channelName; } + + public boolean isPersonal() { return personal; } + public void setPersonal(boolean personal) { this.personal = personal; } + + public BlockRef getChannelRoot() { return channelRoot; } + public void setChannelRoot(BlockRef channelRoot) { this.channelRoot = channelRoot; } + } + + public static class LastMessage { + private BlockRef messageRef; + private String text; + private long createdAtMs; + private String authorLogin; + private String authorBlockchainName; + + public BlockRef getMessageRef() { return messageRef; } + public void setMessageRef(BlockRef messageRef) { this.messageRef = messageRef; } + + public String getText() { return text; } + public void setText(String text) { this.text = text; } + + public long getCreatedAtMs() { return createdAtMs; } + public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; } + + public String getAuthorLogin() { return authorLogin; } + public void setAuthorLogin(String authorLogin) { this.authorLogin = authorLogin; } + + public String getAuthorBlockchainName() { return authorBlockchainName; } + public void setAuthorBlockchainName(String authorBlockchainName) { this.authorBlockchainName = authorBlockchainName; } + } + + public static class BlockRef { + private int blockNumber; + private String blockHash; + + public int getBlockNumber() { return blockNumber; } + public void setBlockNumber(int blockNumber) { this.blockNumber = blockNumber; } + + public String getBlockHash() { return blockHash; } + public void setBlockHash(String blockHash) { this.blockHash = blockHash; } + } +}