SHA256
feat: finalize channels fixes and runtime stability
This commit is contained in:
+5
@@ -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()),
|
||||
|
||||
+45
-31
@@ -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;
|
||||
}
|
||||
|
||||
+14
-2
@@ -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) {}
|
||||
|
||||
+141
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-1
@@ -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();
|
||||
|
||||
+5
@@ -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);
|
||||
}
|
||||
|
||||
+16
-10
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -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; }
|
||||
|
||||
|
||||
+4
@@ -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; }
|
||||
|
||||
|
||||
+4
@@ -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; }
|
||||
|
||||
|
||||
+26
-10
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user