feat: finalize channels fixes and runtime stability

This commit is contained in:
DrygMira
2026-04-13 23:00:36 +03:00
parent 0c7d8fac02
commit a9c69e5947
46 changed files with 4654 additions and 356 deletions
@@ -45,6 +45,7 @@ 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.ChannelNamesStateBootstrapper;
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;
@@ -80,6 +81,10 @@ import java.util.Map;
*/
public final class JsonHandlerRegistry {
static {
ChannelNamesStateBootstrapper.bootstrapOrFailFast();
}
private static final Map<String, JsonMessageHandler> HANDLERS = Map.ofEntries(
Map.entry("AddUser", new Net_AddUser_Handler()),
Map.entry("GetUser", new Net_GetUser_Handler()),
@@ -19,19 +19,21 @@ import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_ut
import server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_utils.BlockchainWriter;
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Request;
import server.logic.ws_protocol.JSON.handlers.blockchain.entyties.Net_AddBlock_Response;
import server.logic.ws_protocol.JSON.handlers.channels.ChannelNamesStateBootstrapper;
import server.logic.ws_protocol.WireCodes;
import shine.db.channels.ChannelNameRules;
import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO;
import shine.db.dao.ChannelNameStateDAO;
import shine.db.dao.UserParamsDAO;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.BlockEntry;
import shine.db.entities.ChannelNameStateEntry;
import shine.db.entities.UserParamEntry;
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;
/**
@@ -49,8 +51,13 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
private final BlocksDAO blocksDAO = BlocksDAO.getInstance();
private final BlockchainStateDAO stateDAO = BlockchainStateDAO.getInstance();
private final UserParamsDAO userParamsDAO = UserParamsDAO.getInstance();
private final ChannelNameStateDAO channelNameStateDAO = ChannelNameStateDAO.getInstance();
private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO, userParamsDAO);
private final BlockchainWriter dbWriter = new BlockchainWriter(blocksDAO, stateDAO, userParamsDAO, channelNameStateDAO);
public Net_AddBlock_Handler() {
ChannelNamesStateBootstrapper.bootstrapOrFailFast();
}
@Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) {
@@ -114,9 +121,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
}
private static String humanMessage(String code) {
if (code == null) return "Ошибка добавления блока";
return switch (code) {
if (code == null) return "Ошибка добавления блока"; return switch (code) {
case "empty_blockchain_name" -> "Пустое имя блокчейна";
case "bad_blockchain_name" -> "Некорректное имя блокчейна";
case "db_error" -> "Ошибка базы данных";
@@ -127,6 +132,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
case "limit_check_failed" -> "Ошибка проверки лимита размера";
case "bad_block_format" -> "Некорректный формат блока";
case "bad_block_body" -> "Некорректное тело блока";
case "bad_channel_name" -> "Некорректное название канала";
case "bad_block_number" -> "Некорректный номер блока";
case "req_global_mismatch" -> "Номер блока в запросе не совпадает с номером в блоке";
case "bad_prev_hash" -> "Некорректный prevHash (цепочка не совпадает)";
@@ -136,7 +142,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 "channel_name_already_exists" -> "Такое название канала уже занято";
case "internal_error" -> "Внутренняя ошибка сервера при записи блока";
default -> "Ошибка: " + code;
};
@@ -237,9 +243,19 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_block_body", serverLastNum, serverLastHashHex);
}
ChannelNameStateEntry channelNameStateEntry = null;
if (block.body instanceof CreateChannelBody createChannelBody) {
final String normalizedName;
final String slug;
try {
if (channelNameExists(blockchainName, createChannelBody.channelName)) {
normalizedName = ChannelNameRules.requireValidDisplayNameForCreate(createChannelBody.channelName);
slug = ChannelNameRules.toCanonicalSlug(normalizedName);
} catch (IllegalArgumentException badName) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, "bad_channel_name", serverLastNum, serverLastHashHex);
}
try {
if (channelNameStateDAO.existsBySlug(slug)) {
return new AddBlockResult(409, "channel_name_already_exists", serverLastNum, serverLastHashHex);
}
} catch (Exception e) {
@@ -247,6 +263,15 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
blockchainName, createChannelBody.channelName, e);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
}
channelNameStateEntry = new ChannelNameStateEntry();
channelNameStateEntry.setSlug(slug);
channelNameStateEntry.setDisplayName(normalizedName);
channelNameStateEntry.setOwnerLogin(login);
channelNameStateEntry.setOwnerBlockchainName(blockchainName);
channelNameStateEntry.setChannelRootBlockNumber(block.blockNumber);
channelNameStateEntry.setChannelRootBlockHash(block.getHash32());
channelNameStateEntry.setCreatedAtMs(block.timestamp * 1000L);
}
// 4.2) запрет дырок: blockNumber строго last+1
@@ -390,9 +415,11 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
);
}
dbWriter.appendBlockAndState(blockchainName, block, st, be, upsertedParam);
dbWriter.appendBlockAndState(blockchainName, block, st, be, upsertedParam, channelNameStateEntry);
} catch (Exception e) {
if (isChannelSlugConflict(e)) {
return new AddBlockResult(409, "channel_name_already_exists", serverLastNum, serverLastHashHex);
}
log.error("AddBlock: внутренняя ошибка при записи блока (login={}, blockchainName={}, blockNumber={})",
login, blockchainName, block.blockNumber, e);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
@@ -415,28 +442,15 @@ 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
}
}
private static boolean isChannelSlugConflict(Throwable throwable) {
Throwable cur = throwable;
while (cur != null) {
String message = String.valueOf(cur.getMessage());
if (message.contains("channel_names_state.slug")
|| message.contains("uq_channel_names_state_slug")) {
return true;
}
cur = cur.getCause();
}
return false;
}
@@ -3,9 +3,11 @@ package server.logic.ws_protocol.JSON.handlers.blockchain.Net_AddBlock_Handler_u
import blockchain.BchBlockEntry;
import shine.db.dao.BlockchainStateDAO;
import shine.db.dao.BlocksDAO;
import shine.db.dao.ChannelNameStateDAO;
import shine.db.dao.UserParamsDAO;
import shine.db.entities.BlockchainStateEntry;
import shine.db.entities.BlockEntry;
import shine.db.entities.ChannelNameStateEntry;
import shine.db.entities.UserParamEntry;
import utils.files.FileStoreUtil;
@@ -23,20 +25,26 @@ public final class BlockchainWriter {
private final BlocksDAO blocksDAO;
private final BlockchainStateDAO stateDAO;
private final ChannelNameStateDAO channelNameStateDAO;
private final UserParamsDAO userParamsDAO;
private final FileStoreUtil fs = FileStoreUtil.getInstance();
public BlockchainWriter(BlocksDAO blocksDAO, BlockchainStateDAO stateDAO, UserParamsDAO userParamsDAO) {
public BlockchainWriter(BlocksDAO blocksDAO,
BlockchainStateDAO stateDAO,
UserParamsDAO userParamsDAO,
ChannelNameStateDAO channelNameStateDAO) {
this.blocksDAO = blocksDAO;
this.stateDAO = stateDAO;
this.userParamsDAO = userParamsDAO;
this.channelNameStateDAO = channelNameStateDAO;
}
public void appendBlockAndState(String blockchainName,
BchBlockEntry block,
BlockchainStateEntry st,
BlockEntry be,
UserParamEntry userParamEntry) throws SQLException {
UserParamEntry userParamEntry,
ChannelNameStateEntry channelNameStateEntry) throws SQLException {
long nowMs = System.currentTimeMillis();
@@ -59,6 +67,10 @@ public final class BlockchainWriter {
userParamsDAO.upsertIfNewer(c, userParamEntry);
}
if (channelNameStateEntry != null) {
channelNameStateDAO.insert(c, channelNameStateEntry);
}
c.commit();
} catch (Exception e) {
try { c.rollback(); } catch (Exception ignored) {}
@@ -0,0 +1,141 @@
package server.logic.ws_protocol.JSON.handlers.channels;
import blockchain.BchBlockEntry;
import blockchain.body.CreateChannelBody;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import shine.db.SqliteDbController;
import shine.db.channels.ChannelNameRules;
import shine.db.dao.ChannelNameStateDAO;
import shine.db.entities.ChannelNameStateEntry;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
public final class ChannelNamesStateBootstrapper {
private static final Logger log = LoggerFactory.getLogger(ChannelNamesStateBootstrapper.class);
private static final int MSG_TYPE_TECH = 0;
private static final int MSG_SUB_TYPE_CREATE_CHANNEL = 1;
private static volatile boolean bootstrapped;
private ChannelNamesStateBootstrapper() {}
public static void bootstrapOrFailFast() {
if (bootstrapped) return;
synchronized (ChannelNamesStateBootstrapper.class) {
if (bootstrapped) return;
rebuildFromBlocksOrThrow();
bootstrapped = true;
}
}
private static void rebuildFromBlocksOrThrow() {
ChannelNameStateDAO dao = ChannelNameStateDAO.getInstance();
List<ChannelNameStateEntry> entries = new ArrayList<>();
Map<String, String> slugToIdentity = new LinkedHashMap<>();
List<String> conflicts = new ArrayList<>();
List<String> skipped = new ArrayList<>();
String sql = """
SELECT login, bch_name, block_number, block_hash, block_bytes
FROM blocks
WHERE msg_type = ? AND msg_sub_type = ?
ORDER BY bch_name, block_number
""";
try (Connection c = SqliteDbController.getInstance().getConnection()) {
c.setAutoCommit(false);
try {
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setInt(1, MSG_TYPE_TECH);
ps.setInt(2, MSG_SUB_TYPE_CREATE_CHANNEL);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
String ownerLogin = rs.getString("login");
String ownerBch = rs.getString("bch_name");
int blockNumber = rs.getInt("block_number");
byte[] blockHash = rs.getBytes("block_hash");
byte[] blockBytes = rs.getBytes("block_bytes");
final BchBlockEntry parsed;
final CreateChannelBody createChannelBody;
try {
parsed = new BchBlockEntry(blockBytes);
if (!(parsed.body instanceof CreateChannelBody ccb)) continue;
createChannelBody = ccb;
} catch (Exception parseError) {
skipped.add(ownerBch + "#" + blockNumber + " (parse_error)");
continue;
}
final String displayName;
final String slug;
try {
displayName = ChannelNameRules.normalizeDisplayName(createChannelBody.channelName);
slug = ChannelNameRules.toCanonicalSlug(displayName);
} catch (Exception badName) {
skipped.add(ownerBch + "#" + blockNumber + " (invalid_name)");
continue;
}
String identity = ownerBch + "#" + blockNumber;
String existing = slugToIdentity.putIfAbsent(slug, identity);
if (existing != null && !existing.equals(identity)) {
conflicts.add("slug=\"" + slug + "\" conflicts: " + existing + " vs " + identity);
continue;
}
ChannelNameStateEntry entry = new ChannelNameStateEntry();
entry.setSlug(slug);
entry.setDisplayName(displayName);
entry.setOwnerLogin(ownerLogin);
entry.setOwnerBlockchainName(ownerBch);
entry.setChannelRootBlockNumber(blockNumber);
entry.setChannelRootBlockHash(blockHash);
entry.setCreatedAtMs(parsed.timestamp * 1000L);
entries.add(entry);
}
}
}
dao.clearAll(c);
dao.insertAll(c, entries);
c.commit();
log.info("channel_names_state bootstrapped: {}", entries.size());
if (!conflicts.isEmpty()) {
log.warn("channel_names_state bootstrap detected {} slug conflicts (kept first occurrence)", conflicts.size());
int preview = Math.min(conflicts.size(), 10);
for (int i = 0; i < preview; i++) {
log.warn("channel_names_state conflict: {}", conflicts.get(i));
}
}
if (!skipped.isEmpty()) {
log.warn("channel_names_state bootstrap skipped {} legacy entries", skipped.size());
int preview = Math.min(skipped.size(), 10);
for (int i = 0; i < preview; i++) {
log.warn("channel_names_state skipped: {}", skipped.get(i));
}
}
} catch (Exception e) {
try {
c.rollback();
} catch (Exception ignored) {
}
throw e;
} finally {
try {
c.setAutoCommit(true);
} catch (Exception ignored) {
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to bootstrap channel_names_state", e);
}
}
}
@@ -4,6 +4,8 @@ import blockchain.BchBlockEntry;
import blockchain.body.BodyRecord;
import blockchain.body.CreateChannelBody;
import blockchain.body.TextBody;
import blockchain.body.TextLineBody;
import blockchain.body.TextReplyBody;
import shine.db.MsgSubType;
import java.sql.Connection;
@@ -15,6 +17,7 @@ import java.util.List;
final class ChannelsReadSupport {
static final int MSG_TYPE_TEXT = 1;
static final int MSG_TYPE_REACTION = 2;
static final int MSG_TYPE_TECH = 0;
private ChannelsReadSupport() {}
@@ -122,7 +125,11 @@ final class ChannelsReadSupport {
BchBlockEntry e = new BchBlockEntry(blockBytes);
TextInfo ti = new TextInfo();
ti.createdAtMs = e.timestamp * 1000L;
if (e.body instanceof TextBody tb) {
if (e.body instanceof TextLineBody tlb) {
ti.text = tlb.message;
} else if (e.body instanceof TextReplyBody trb) {
ti.text = trb.message;
} else if (e.body instanceof TextBody tb) {
ti.text = tb.message;
}
return ti;
@@ -205,6 +212,34 @@ final class ChannelsReadSupport {
}
}
static boolean isLikedByLogin(Connection c, String login, String toBch, int toBlockNumber, byte[] toBlockHash) throws SQLException {
if (login == null || login.isBlank() || toBch == null || toBch.isBlank() || toBlockHash == null || toBlockHash.length != 32) {
return false;
}
String sql = """
SELECT msg_sub_type
FROM blocks
WHERE login = ? COLLATE NOCASE
AND msg_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, login);
ps.setInt(2, MSG_TYPE_REACTION);
ps.setString(3, toBch);
ps.setInt(4, toBlockNumber);
ps.setBytes(5, toBlockHash);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return false;
return rs.getInt("msg_sub_type") == MsgSubType.REACTION_LIKE;
}
}
}
static byte[] hexToBytes(String s) {
if (s == null) return null;
String x = s.trim();
@@ -38,6 +38,10 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
boolean asc = req.getSort() == null || !"desc".equalsIgnoreCase(req.getSort());
try (Connection c = SqliteDbController.getInstance().getConnection()) {
String viewerLogin = ctx != null ? ctx.getLogin() : null;
if (viewerLogin == null || viewerLogin.isBlank()) {
viewerLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin());
}
String ownerBch = req.getChannel().getOwnerBlockchainName();
int lineCode = req.getChannel().getChannelRootBlockNumber();
@@ -102,6 +106,7 @@ public class Net_GetChannelMessages_Handler implements JsonMessageHandler {
int[] stats = ChannelsReadSupport.loadStats(c, ownerBch, post.blockNumber, post.blockHash);
item.setLikesCount(stats[0]);
item.setRepliesCount(stats[1]);
item.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, post.bchName, post.blockNumber, post.blockHash));
items.add(item);
}
@@ -27,7 +27,7 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
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");
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля message");
}
int depthUp = req.getDepthUp() == null ? 20 : Math.max(0, req.getDepthUp());
@@ -35,9 +35,13 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
int childLimit = req.getLimitChildrenPerNode() == null ? 50 : Math.max(1, req.getLimitChildrenPerNode());
try (Connection c = SqliteDbController.getInstance().getConnection()) {
String viewerLogin = ctx != null ? ctx.getLogin() : null;
if (viewerLogin == null || viewerLogin.isBlank()) {
viewerLogin = ChannelsReadSupport.canonicalLogin(c, req.getLogin());
}
PostRow focusRow = findByNumber(c, req.getMessage().getBlockchainName(), req.getMessage().getBlockNumber());
if (focusRow == null) {
return NetExceptionResponseFactory.error(req, 404, "message_not_found", "Сообщение не найдено");
return NetExceptionResponseFactory.error(req, 404, "message_not_found", "Сообщение не найдено");
}
Net_GetMessageThread_Response resp = new Net_GetMessageThread_Response();
@@ -45,7 +49,7 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setFocus(toNode(c, focusRow));
resp.setFocus(toNode(c, focusRow, viewerLogin));
List<Net_GetMessageThread_Response.MessageNode> ancestors = new ArrayList<>();
PostRow cur = focusRow;
@@ -53,27 +57,27 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
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));
ancestors.add(0, toNode(c, parent, viewerLogin));
cur = parent;
}
resp.setAncestors(ancestors);
resp.setDescendants(loadChildren(c, focusRow, depthDown, childLimit));
resp.setDescendants(loadChildren(c, focusRow, depthDown, childLimit, viewerLogin));
return resp;
} catch (Exception e) {
log.error("GetMessageThread failed", e);
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
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 {
private List<Net_GetMessageThread_Response.MessageNodeTree> loadChildren(Connection c, PostRow parent, int depthDown, int childLimit, String viewerLogin) 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));
t.setNode(toNode(c, row, viewerLogin));
t.setChildren(loadChildren(c, row, depthDown - 1, childLimit, viewerLogin));
out.add(t);
}
return out;
@@ -133,7 +137,7 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
return row;
}
private Net_GetMessageThread_Response.MessageNode toNode(Connection c, PostRow row) throws Exception {
private Net_GetMessageThread_Response.MessageNode toNode(Connection c, PostRow row, String viewerLogin) 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);
@@ -173,6 +177,7 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
int[] stats = ChannelsReadSupport.loadStats(c, row.bchName, row.blockNumber, row.blockHash);
node.setLikesCount(stats[0]);
node.setRepliesCount(stats[1]);
node.setLikedByMe(ChannelsReadSupport.isLikedByLogin(c, viewerLogin, row.bchName, row.blockNumber, row.blockHash));
if (row.lineCode != null && row.lineCode >= 0) {
Net_GetMessageThread_Response.ChannelInfo ci = new Net_GetMessageThread_Response.ChannelInfo();
@@ -222,3 +227,4 @@ public class Net_GetMessageThread_Handler implements JsonMessageHandler {
int msgSubType;
}
}
@@ -3,10 +3,14 @@ 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 String login;
private ChannelSelector channel;
private Integer limit;
private String sort;
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
public ChannelSelector getChannel() { return channel; }
public void setChannel(ChannelSelector channel) { this.channel = channel; }
@@ -41,6 +41,7 @@ public class Net_GetChannelMessages_Response extends Net_Response {
private long createdAtMs;
private String text;
private int likesCount;
private boolean likedByMe;
private int repliesCount;
private int versionsTotal;
private List<VersionItem> versions = new ArrayList<>();
@@ -63,6 +64,9 @@ public class Net_GetChannelMessages_Response extends Net_Response {
public int getLikesCount() { return likesCount; }
public void setLikesCount(int likesCount) { this.likesCount = likesCount; }
public boolean isLikedByMe() { return likedByMe; }
public void setLikedByMe(boolean likedByMe) { this.likedByMe = likedByMe; }
public int getRepliesCount() { return repliesCount; }
public void setRepliesCount(int repliesCount) { this.repliesCount = repliesCount; }
@@ -3,11 +3,15 @@ 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 String login;
private MessageSelector message;
private Integer depthUp;
private Integer depthDown;
private Integer limitChildrenPerNode;
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
public MessageSelector getMessage() { return message; }
public void setMessage(MessageSelector message) { this.message = message; }
@@ -9,7 +9,6 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_AddCloseF
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.MsgSubType;
import shine.db.dao.ConnectionsStateDAO;
import java.sql.Connection;
import java.sql.PreparedStatement;
@@ -42,15 +41,10 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
return NetExceptionResponseFactory.error(req, 404, "BLOCKCHAIN_NOT_FOUND", "У пользователя нет blockchain");
}
ConnectionsStateDAO.getInstance().upsertRelation(
c,
from,
MsgSubType.CONNECTION_FRIEND,
canonicalTo,
targetBch,
0,
new byte[32]
);
// Idempotent insert for close-friend relation.
// Using INSERT OR IGNORE avoids ON CONFLICT(column list) mismatches
// across DB instances with different UNIQUE schemas.
insertCloseFriendIgnoreDuplicate(c, from, canonicalTo, targetBch);
Net_AddCloseFriend_Response resp = new Net_AddCloseFriend_Response();
resp.setOp(req.getOp());
@@ -82,4 +76,26 @@ public class Net_AddCloseFriend_Handler implements JsonMessageHandler {
}
}
}
private void insertCloseFriendIgnoreDuplicate(Connection c,
String login,
String toLogin,
String toBchName) throws Exception {
String sql = """
INSERT OR IGNORE INTO connections_state (
login, rel_type, to_login, to_bch_name, to_block_number, to_block_hash
)
VALUES (?, ?, ?, ?, ?, ?)
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, login);
ps.setInt(2, MsgSubType.CONNECTION_FRIEND);
ps.setString(3, toLogin);
ps.setString(4, toBchName);
ps.setInt(5, 0);
ps.setBytes(6, new byte[32]);
ps.executeUpdate();
}
}
}
@@ -23,9 +23,17 @@ public final class AuthKeyUtils {
public static byte[] parseEd25519PublicKey(String key, String fieldName) {
String normalized = normalize(key, fieldName);
// Legacy format is plain BASE64(32 bytes) and may contain '/' characters.
// Try legacy decode first to avoid misinterpreting base64 payload as algorithm prefix.
try {
return Base64Ws.decodeLen(normalized, 32, fieldName);
} catch (IllegalArgumentException ignored) {
// continue with explicit algorithm/key format
}
int slash = normalized.indexOf('/');
if (slash < 0) {
return Base64Ws.decodeLen(normalized, 32, fieldName);
throw new IllegalArgumentException(fieldName + " has bad base64/key format");
}
String algorithm = normalized.substring(0, slash).trim();