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 = `
+
+
+
${kindLabel}
+
+
+
+
+
+
+
+
+ `;
+
+ 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; }
+ }
+}