Start server-side channel read RPC handlers and simplify API spec
This commit is contained in:
parent
eb5593c7be
commit
9723696b2c
208
Dev_Docs/API/06_Channels_Read_API.md
Normal file
208
Dev_Docs/API/06_Channels_Read_API.md
Normal file
@ -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`
|
||||||
@ -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 chatView from './pages/chat-view.js?v=20260330001044';
|
||||||
import * as channelsList from './pages/channels-list.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 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 networkView from './pages/network-view.js?v=20260330001044';
|
||||||
import * as notificationsView from './pages/notifications-view.js?v=20260330001044';
|
import * as notificationsView from './pages/notifications-view.js?v=20260330001044';
|
||||||
|
|
||||||
@ -69,6 +70,7 @@ const routes = {
|
|||||||
'chat-view': chatView,
|
'chat-view': chatView,
|
||||||
'channels-list': channelsList,
|
'channels-list': channelsList,
|
||||||
'channel-view': channelView,
|
'channel-view': channelView,
|
||||||
|
'add-channel-view': addChannelView,
|
||||||
'network-view': networkView,
|
'network-view': networkView,
|
||||||
'notifications-view': notificationsView,
|
'notifications-view': notificationsView,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -147,35 +147,80 @@ export const chatMessages = {
|
|||||||
|
|
||||||
export const channels = [
|
export const channels = [
|
||||||
{
|
{
|
||||||
id: 'ch1',
|
id: 'ch0',
|
||||||
name: 'Новости продукта',
|
name: 'Личный канал',
|
||||||
initials: 'НП',
|
initials: 'ЛК',
|
||||||
description: 'Официальный канал команды Shine с релизами и обновлениями.',
|
ownerLogin: '@shine.alex',
|
||||||
lastMessage: 'Опубликовали обзор нового демо-прототипа мобильного интерфейса.',
|
ownerName: 'Вы',
|
||||||
|
description: 'Ваш основной канал (нулевой).',
|
||||||
|
lastMessage: 'Добро пожаловать в личный канал.',
|
||||||
time: '16:05',
|
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',
|
id: 'ch2',
|
||||||
name: 'Анекдоты дня',
|
name: 'Новости Bob',
|
||||||
initials: 'АД',
|
initials: 'NB',
|
||||||
description: 'Лёгкий развлекательный канал с короткими шутками и мемами.',
|
ownerLogin: '@bob',
|
||||||
lastMessage: 'Новый пост: как дизайнер, разработчик и дедлайн зашли в бар.',
|
ownerName: 'Bob',
|
||||||
|
description: 'Основной канал пользователя Bob.',
|
||||||
|
lastMessage: 'Вышел новый дайджест разработчика.',
|
||||||
time: '15:20',
|
time: '15:20',
|
||||||
unread: 3,
|
messagesCount: 5,
|
||||||
|
kind: 'followed-user-channel',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'ch3',
|
id: 'ch3',
|
||||||
name: 'Новости рынка',
|
name: 'Стендап команды Bob',
|
||||||
initials: 'НР',
|
initials: 'SB',
|
||||||
description: 'Короткие ежедневные сводки по рынку, технологиям и сообществам.',
|
ownerLogin: '@bob',
|
||||||
lastMessage: 'В ленте свежая подборка новостей и главных событий дня.',
|
ownerName: 'Bob',
|
||||||
|
description: 'Второй канал пользователя Bob.',
|
||||||
|
lastMessage: 'Перенесли созвон на 19:30.',
|
||||||
time: 'вчера',
|
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 = {
|
export const channelPosts = {
|
||||||
|
ch0: [
|
||||||
|
{
|
||||||
|
id: 'p0-1',
|
||||||
|
title: 'Первый личный пост',
|
||||||
|
body: 'Этот канал всегда ваш и стоит в списке первым.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p0-2',
|
||||||
|
title: 'Планы',
|
||||||
|
body: 'Сюда удобно сохранять личные заметки и объявления.',
|
||||||
|
},
|
||||||
|
],
|
||||||
ch1: [
|
ch1: [
|
||||||
{
|
{
|
||||||
id: 'p1',
|
id: 'p1',
|
||||||
|
|||||||
38
shine-UI/js/pages/add-channel-view.js
Normal file
38
shine-UI/js/pages/add-channel-view.js
Normal file
@ -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 = `
|
||||||
|
<label for="channel-name">Имя канала</label>
|
||||||
|
<input id="channel-name" class="input" maxlength="64" placeholder="Например: Новости команды" required />
|
||||||
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
|
||||||
|
<button type="button" class="secondary-btn" id="cancel-create-channel">Отмена</button>
|
||||||
|
<button type="submit" class="primary-btn">Создать</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
form.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
navigate('channels-list');
|
||||||
|
});
|
||||||
|
|
||||||
|
form.querySelector('#cancel-create-channel').addEventListener('click', () => {
|
||||||
|
navigate('channels-list');
|
||||||
|
});
|
||||||
|
|
||||||
|
screen.append(form);
|
||||||
|
return screen;
|
||||||
|
}
|
||||||
@ -7,6 +7,7 @@ export function render({ navigate, route }) {
|
|||||||
const channelId = route.params.channelId || 'ch1';
|
const channelId = route.params.channelId || 'ch1';
|
||||||
const channel = channels.find((c) => c.id === channelId) || channels[0];
|
const channel = channels.find((c) => c.id === channelId) || channels[0];
|
||||||
const posts = channelPosts[channelId] || [];
|
const posts = channelPosts[channelId] || [];
|
||||||
|
const isOwnChannel = channel.ownerLogin === '@shine.alex';
|
||||||
|
|
||||||
const screen = document.createElement('section');
|
const screen = document.createElement('section');
|
||||||
screen.className = 'stack';
|
screen.className = 'stack';
|
||||||
@ -23,9 +24,13 @@ export function render({ navigate, route }) {
|
|||||||
head.innerHTML = `
|
head.innerHTML = `
|
||||||
<strong># ${channel.name}</strong>
|
<strong># ${channel.name}</strong>
|
||||||
<p class="meta-muted" style="margin-top:4px;">${channel.description}</p>
|
<p class="meta-muted" style="margin-top:4px;">${channel.description}</p>
|
||||||
<p class="meta-muted" style="margin-top:8px;">Публичный канал, режим только чтение</p>
|
<p class="meta-muted" style="margin-top:8px;">Владелец: ${channel.ownerName}</p>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const actionButton = document.createElement('button');
|
||||||
|
actionButton.className = isOwnChannel ? 'primary-btn' : 'secondary-btn';
|
||||||
|
actionButton.textContent = isOwnChannel ? 'Добавить сообщение в канал' : 'Отписаться от канала';
|
||||||
|
|
||||||
const feed = document.createElement('div');
|
const feed = document.createElement('div');
|
||||||
feed.className = 'stack';
|
feed.className = 'stack';
|
||||||
|
|
||||||
@ -36,6 +41,6 @@ export function render({ navigate, route }) {
|
|||||||
feed.append(card);
|
feed.append(card);
|
||||||
});
|
});
|
||||||
|
|
||||||
screen.append(head, feed);
|
screen.append(head, actionButton, feed);
|
||||||
return screen;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,21 +3,31 @@ import { channels } from '../mock-data.js?v=20260330001044';
|
|||||||
|
|
||||||
export const pageMeta = { id: 'channels-list', title: 'Каналы' };
|
export const pageMeta = { id: 'channels-list', title: 'Каналы' };
|
||||||
|
|
||||||
export function render({ navigate }) {
|
function openSimpleSubscribeModal(kindLabel) {
|
||||||
const screen = document.createElement('section');
|
const root = document.getElementById('modal-root');
|
||||||
screen.className = 'stack';
|
root.innerHTML = `
|
||||||
|
<div class="modal" id="channels-subscribe-modal">
|
||||||
|
<div class="modal-card stack">
|
||||||
|
<h3 style="font-size:18px;">${kindLabel}</h3>
|
||||||
|
<label class="meta-muted" for="subscribe-input">Введите идентификатор</label>
|
||||||
|
<input id="subscribe-input" class="input" placeholder="@login или #канал" />
|
||||||
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:10px;">
|
||||||
|
<button class="secondary-btn" id="sub-cancel">Отмена</button>
|
||||||
|
<button class="primary-btn" id="sub-submit">Подписаться</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
screen.append(renderHeader({ title: 'Каналы' }));
|
const close = () => {
|
||||||
|
root.innerHTML = '';
|
||||||
|
};
|
||||||
|
|
||||||
const search = document.createElement('div');
|
root.querySelector('#sub-cancel').addEventListener('click', close);
|
||||||
search.className = 'card';
|
root.querySelector('#sub-submit').addEventListener('click', close);
|
||||||
search.textContent = 'Найти канал';
|
}
|
||||||
search.style.color = 'var(--text-muted)';
|
|
||||||
|
|
||||||
const list = document.createElement('div');
|
function renderChannelRow(channel, navigate) {
|
||||||
list.className = 'stack';
|
|
||||||
|
|
||||||
channels.forEach((channel) => {
|
|
||||||
const row = document.createElement('article');
|
const row = document.createElement('article');
|
||||||
row.className = 'list-item';
|
row.className = 'list-item';
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
@ -26,17 +36,91 @@ export function render({ navigate }) {
|
|||||||
<strong># ${channel.name}</strong>
|
<strong># ${channel.name}</strong>
|
||||||
<p class="meta-muted" style="margin-top:4px;">${channel.description}</p>
|
<p class="meta-muted" style="margin-top:4px;">${channel.description}</p>
|
||||||
<p class="meta-muted" style="margin-top:6px; color:#d8e3ff;">${channel.lastMessage}</p>
|
<p class="meta-muted" style="margin-top:6px; color:#d8e3ff;">${channel.lastMessage}</p>
|
||||||
|
<p class="meta-muted" style="margin-top:6px;">Владелец: ${channel.ownerName}</p>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:grid; justify-items:end; gap:6px;">
|
<div style="display:grid; justify-items:end; gap:6px;">
|
||||||
<span class="badge alt" style="padding:4px 8px; font-size:10px;">Канал</span>
|
<span class="badge alt" style="padding:4px 8px; font-size:10px;">Канал</span>
|
||||||
<span class="meta-muted">${channel.time}</span>
|
<span class="meta-muted">${channel.time}</span>
|
||||||
${channel.unread ? `<span class="unread">${channel.unread}</span>` : '<span></span>'}
|
<span class="unread">${channel.messagesCount}</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
row.addEventListener('click', () => navigate(`channel-view/${channel.id}`));
|
row.addEventListener('click', () => navigate(`channel-view/${channel.id}`));
|
||||||
list.append(row);
|
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));
|
||||||
});
|
});
|
||||||
|
|
||||||
screen.append(search, list);
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function render({ navigate }) {
|
||||||
|
const screen = document.createElement('section');
|
||||||
|
screen.className = 'stack';
|
||||||
|
|
||||||
|
screen.append(
|
||||||
|
renderHeader({
|
||||||
|
title: 'Каналы',
|
||||||
|
rightActions: [
|
||||||
|
{ label: 'Подписаться на человека', onClick: () => openSimpleSubscribeModal('Подписка на человека') },
|
||||||
|
{ label: 'Подписаться на канал', onClick: () => openSimpleSubscribeModal('Подписка на канал') },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
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 channels-groups';
|
||||||
|
|
||||||
|
list.append(renderSection('Мои каналы', ownChannels, navigate));
|
||||||
|
|
||||||
|
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;
|
return screen;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,6 +57,6 @@ export function resolveToolbarActive(pageId) {
|
|||||||
return 'profile-view';
|
return 'profile-view';
|
||||||
}
|
}
|
||||||
if (pageId === 'chat-view' || pageId === 'contact-search-view') return 'messages-list';
|
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';
|
return 'profile-view';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -728,3 +728,38 @@
|
|||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 14px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@ -45,6 +45,12 @@ import server.logic.ws_protocol.JSON.handlers.userParams.entyties.Net_UpsertUser
|
|||||||
// --- NEW: connections friends lists ---
|
// --- NEW: connections friends lists ---
|
||||||
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetFriendsLists_Handler;
|
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.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 ---
|
// --- NEW: Ping ---
|
||||||
import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler;
|
import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler;
|
||||||
@ -85,6 +91,9 @@ public final class JsonHandlerRegistry {
|
|||||||
|
|
||||||
// --- connections ---
|
// --- connections ---
|
||||||
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
|
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 ---
|
// --- system ---
|
||||||
Map.entry("Ping", new Net_Ping_Handler()),
|
Map.entry("Ping", new Net_Ping_Handler()),
|
||||||
@ -119,6 +128,9 @@ public final class JsonHandlerRegistry {
|
|||||||
|
|
||||||
// --- connections ---
|
// --- connections ---
|
||||||
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
|
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 ---
|
// --- system ---
|
||||||
Map.entry("Ping", Net_Ping_Request.class),
|
Map.entry("Ping", Net_Ping_Request.class),
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import blockchain.BchCryptoVerifier;
|
|||||||
import blockchain.MsgSubType;
|
import blockchain.MsgSubType;
|
||||||
import blockchain.body.BodyHasLine;
|
import blockchain.body.BodyHasLine;
|
||||||
import blockchain.body.BodyHasTarget;
|
import blockchain.body.BodyHasTarget;
|
||||||
|
import blockchain.body.CreateChannelBody;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import server.logic.ws_protocol.Base64Ws;
|
import server.logic.ws_protocol.Base64Ws;
|
||||||
@ -25,6 +26,9 @@ import shine.db.entities.BlockEntry;
|
|||||||
import utils.blockchain.BlockchainNameUtil;
|
import utils.blockchain.BlockchainNameUtil;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
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 "prev_line_block_not_found" -> "Не найден блок prevLineNumber для проверки линии";
|
||||||
case "bad_prev_line_hash" -> "Некорректный prevLineHash";
|
case "bad_prev_line_hash" -> "Некорректный prevLineHash";
|
||||||
case "db_error_prev_line_check" -> "Ошибка БД при проверке prevLine";
|
case "db_error_prev_line_check" -> "Ошибка БД при проверке prevLine";
|
||||||
|
case "channel_name_already_exists" -> "Канал с таким именем уже существует";
|
||||||
case "internal_error" -> "Внутренняя ошибка сервера при записи блока";
|
case "internal_error" -> "Внутренняя ошибка сервера при записи блока";
|
||||||
default -> "Ошибка: " + code;
|
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);
|
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
|
// 4.2) запрет дырок: blockNumber строго last+1
|
||||||
int expectedBlockNumber = serverLastNum + 1;
|
int expectedBlockNumber = serverLastNum + 1;
|
||||||
if (block.blockNumber != expectedBlockNumber) {
|
if (block.blockNumber != expectedBlockNumber) {
|
||||||
@ -378,6 +395,32 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
|
|||||||
return Base64Ws.decode(b64);
|
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) {
|
private static long safeAdd(long a, long b) {
|
||||||
long r = a + b;
|
long r = a + b;
|
||||||
if (((a ^ r) & (b ^ r)) < 0) throw new ArithmeticException("long overflow");
|
if (((a ^ r) & (b ^ r)) < 0) throw new ArithmeticException("long overflow");
|
||||||
|
|||||||
@ -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<PostBlock> 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<PostBlock> 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<PostBlock> 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<PostBlock> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<ChannelsReadSupport.PostBlock> posts = ChannelsReadSupport.channelPosts(c, ownerBch, lineCode, limit, asc);
|
||||||
|
List<Net_GetChannelMessages_Response.MessageItem> 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<Net_GetChannelMessages_Response.VersionItem> 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<ChannelsReadSupport.PostBlock> 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", "Внутренняя ошибка сервера");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<Net_GetMessageThread_Response.MessageNode> 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<Net_GetMessageThread_Response.MessageNodeTree> loadChildren(Connection c, PostRow parent, int depthDown, int childLimit) throws Exception {
|
||||||
|
if (depthDown <= 0) return List.of();
|
||||||
|
List<PostRow> replies = findReplies(c, parent.bchName, parent.blockNumber, parent.blockHash, childLimit);
|
||||||
|
List<Net_GetMessageThread_Response.MessageNodeTree> 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<PostRow> 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<PostRow> 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<Net_GetChannelMessages_Response.VersionItem> 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<PostRow> 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<PostRow> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<ChannelKey> own = loadOwnChannels(c, canonicalLogin);
|
||||||
|
List<ChannelKey> followedUsersChannels = loadFollowedChannels(c, canonicalLogin, true);
|
||||||
|
List<ChannelKey> 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<Net_ListSubscriptionsFeed_Response.ChannelSummary> buildSummaries(Connection c, List<ChannelKey> keys) throws Exception {
|
||||||
|
List<Net_ListSubscriptionsFeed_Response.ChannelSummary> 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<ChannelKey> loadOwnChannels(Connection c, String canonicalLogin) throws Exception {
|
||||||
|
List<ChannelKey> 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<ChannelKey> loadFollowedChannels(Connection c, String canonicalLogin, boolean onlyUserRoots) throws Exception {
|
||||||
|
List<ChannelKey> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<MessageItem> messages = new ArrayList<>();
|
||||||
|
|
||||||
|
public Channel getChannel() { return channel; }
|
||||||
|
public void setChannel(Channel channel) { this.channel = channel; }
|
||||||
|
|
||||||
|
public List<MessageItem> getMessages() { return messages; }
|
||||||
|
public void setMessages(List<MessageItem> 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<VersionItem> 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<VersionItem> getVersions() { return versions; }
|
||||||
|
public void setVersions(List<VersionItem> 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<MessageNode> ancestors = new ArrayList<>();
|
||||||
|
private MessageNode focus;
|
||||||
|
private List<MessageNodeTree> descendants = new ArrayList<>();
|
||||||
|
|
||||||
|
public List<MessageNode> getAncestors() { return ancestors; }
|
||||||
|
public void setAncestors(List<MessageNode> ancestors) { this.ancestors = ancestors; }
|
||||||
|
|
||||||
|
public MessageNode getFocus() { return focus; }
|
||||||
|
public void setFocus(MessageNode focus) { this.focus = focus; }
|
||||||
|
|
||||||
|
public List<MessageNodeTree> getDescendants() { return descendants; }
|
||||||
|
public void setDescendants(List<MessageNodeTree> 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<MessageNodeTree> children = new ArrayList<>();
|
||||||
|
|
||||||
|
public MessageNode getNode() { return node; }
|
||||||
|
public void setNode(MessageNode node) { this.node = node; }
|
||||||
|
|
||||||
|
public List<MessageNodeTree> getChildren() { return children; }
|
||||||
|
public void setChildren(List<MessageNodeTree> children) { this.children = children; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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; }
|
||||||
|
}
|
||||||
@ -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<ChannelSummary> ownedChannels = new ArrayList<>();
|
||||||
|
private List<ChannelSummary> followedUsersChannels = new ArrayList<>();
|
||||||
|
private List<ChannelSummary> followedChannels = new ArrayList<>();
|
||||||
|
|
||||||
|
public String getLogin() { return login; }
|
||||||
|
public void setLogin(String login) { this.login = login; }
|
||||||
|
|
||||||
|
public List<ChannelSummary> getOwnedChannels() { return ownedChannels; }
|
||||||
|
public void setOwnedChannels(List<ChannelSummary> ownedChannels) { this.ownedChannels = ownedChannels; }
|
||||||
|
|
||||||
|
public List<ChannelSummary> getFollowedUsersChannels() { return followedUsersChannels; }
|
||||||
|
public void setFollowedUsersChannels(List<ChannelSummary> followedUsersChannels) { this.followedUsersChannels = followedUsersChannels; }
|
||||||
|
|
||||||
|
public List<ChannelSummary> getFollowedChannels() { return followedChannels; }
|
||||||
|
public void setFollowedChannels(List<ChannelSummary> 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user