Добавить дневник действий и статусы материалов

This commit is contained in:
AidarKC
2026-08-10 01:34:17 +04:00
parent ee185cf20a
commit 67d63256c2
20 changed files with 881 additions and 90 deletions
@@ -160,11 +160,12 @@ public final class MsgSubType {
/* ===================== STATUS_ACTION (msg_type=5) ===================== */
public static final short STATUS_DONE_ONCE = 10;
public static final short STATUS_INTERESTED = 20;
public static final short STATUS_STARTED = 30;
public static final short STATUS_IN_STUDY = 40;
public static final short STATUS_COMPLETED = 50;
public static final short STATUS_ABANDONED = 60;
public static final short STATUS_LEARNED = 70;
public static final short STATUS_CONFIRMED = 80;
public static final short STATUS_LEARNED = 20;
public static final short STATUS_SERVICE_PASSED = 30;
public static final short STATUS_CONFIRMED = 100;
public static final short STATUS_INTERESTED = 110;
public static final short STATUS_STARTED = 120;
public static final short STATUS_IN_STUDY = 130;
public static final short STATUS_ABANDONED = 140;
public static final short STATUS_COMPLETED = 150;
}
@@ -127,13 +127,14 @@ public final class StatusActionBody implements BodyRecord, BodyHasTarget {
private static boolean isSupportedSubType(short subType) {
int st = subType & 0xFFFF;
return st == (MsgSubType.STATUS_DONE_ONCE & 0xFFFF)
|| st == (MsgSubType.STATUS_LEARNED & 0xFFFF)
|| st == (MsgSubType.STATUS_SERVICE_PASSED & 0xFFFF)
|| st == (MsgSubType.STATUS_CONFIRMED & 0xFFFF)
|| st == (MsgSubType.STATUS_INTERESTED & 0xFFFF)
|| st == (MsgSubType.STATUS_STARTED & 0xFFFF)
|| st == (MsgSubType.STATUS_IN_STUDY & 0xFFFF)
|| st == (MsgSubType.STATUS_COMPLETED & 0xFFFF)
|| st == (MsgSubType.STATUS_ABANDONED & 0xFFFF)
|| st == (MsgSubType.STATUS_LEARNED & 0xFFFF)
|| st == (MsgSubType.STATUS_CONFIRMED & 0xFFFF);
|| st == (MsgSubType.STATUS_COMPLETED & 0xFFFF);
}
private static String readStrictUtf8Len16AllowEmpty(ByteBuffer bb, String fieldName) {
@@ -138,13 +138,14 @@ public final class MsgSubType {
/* ===================== STATUS_ACTION (msg_type=5) ===================== */
public static final short STATUS_DONE_ONCE = 10;
public static final short STATUS_INTERESTED = 20;
public static final short STATUS_STARTED = 30;
public static final short STATUS_IN_STUDY = 40;
public static final short STATUS_COMPLETED = 50;
public static final short STATUS_ABANDONED = 60;
public static final short STATUS_LEARNED = 70;
public static final short STATUS_CONFIRMED = 80;
public static final short STATUS_LEARNED = 20;
public static final short STATUS_SERVICE_PASSED = 30;
public static final short STATUS_CONFIRMED = 100;
public static final short STATUS_INTERESTED = 110;
public static final short STATUS_STARTED = 120;
public static final short STATUS_IN_STUDY = 130;
public static final short STATUS_ABANDONED = 140;
public static final short STATUS_COMPLETED = 150;
/* ===================== РЕЗЕРВ НА БУДУЩЕЕ ===================== */
// Если позже захочешь BLOCK/UNBLOCK — лучше добавить новые значения,
@@ -67,6 +67,7 @@ import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetFriend
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_GetPersonalDiary_Handler;
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetGroupDialog_Handler;
import server.logic.ws_protocol.JSON.handlers.channels.Net_GetChannelsCounters_Handler;
import server.logic.ws_protocol.JSON.handlers.channels.Net_ListGroupChats200_Handler;
@@ -75,6 +76,7 @@ import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelsC
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Request;
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetGroupDialog_Request;
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageThread_Request;
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetPersonalDiary_Request;
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListGroupChats200_Request;
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request;
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetUserConnectionsGraph_Handler;
@@ -184,6 +186,7 @@ public final class JsonHandlerRegistry {
Map.entry("GetFriendsLists", new Net_GetFriendsLists_Handler()),
Map.entry("ListSubscriptionsFeed", new Net_ListSubscriptionsFeed_Handler()),
Map.entry("GetChannelMessages", new Net_GetChannelMessages_Handler()),
Map.entry("GetPersonalDiary", new Net_GetPersonalDiary_Handler()),
Map.entry("GetMessageThread", new Net_GetMessageThread_Handler()),
Map.entry("GetGroupDialog", new Net_GetGroupDialog_Handler()),
Map.entry("ListGroupChats200", new Net_ListGroupChats200_Handler()),
@@ -265,6 +268,7 @@ public final class JsonHandlerRegistry {
Map.entry("GetFriendsLists", Net_GetFriendsLists_Request.class),
Map.entry("ListSubscriptionsFeed", Net_ListSubscriptionsFeed_Request.class),
Map.entry("GetChannelMessages", Net_GetChannelMessages_Request.class),
Map.entry("GetPersonalDiary", Net_GetPersonalDiary_Request.class),
Map.entry("GetMessageThread", Net_GetMessageThread_Request.class),
Map.entry("GetGroupDialog", Net_GetGroupDialog_Request.class),
Map.entry("ListGroupChats200", Net_ListGroupChats200_Request.class),
@@ -170,6 +170,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
case "repost_disabled" -> "Репосты временно отключены до будущей реализации";
case "entrypoint_edit_forbidden" -> "TEXT_ENTRYPOINT нельзя редактировать через TEXT_EDIT_POST";
case "status_confirmed_target_must_be_status_action" -> "STATUS_CONFIRMED должен ссылаться на STATUS_ACTION";
case "status_action_target_not_allowed" -> "Этот STATUS_ACTION нельзя ставить на выбранный тип материала";
case "internal_error" -> "Внутренняя ошибка сервера при записи блока";
case "chain_resync_in_progress" -> "Цепочка сейчас пересинхронизируется";
default -> "Ошибка: " + code;
@@ -405,15 +406,14 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
}
}
if ((block.type & 0xFFFF) == 5
&& (block.subType & 0xFFFF) == (MsgSubType.STATUS_CONFIRMED & 0xFFFF)) {
if ((block.type & 0xFFFF) == 5) {
try {
String statusError = validateStatusConfirmedTarget(block);
String statusError = validateStatusActionTarget(block);
if (statusError != null) {
return new AddBlockResult(WireCodes.Status.BAD_REQUEST, statusError, serverLastNum, serverLastHashHex);
}
} catch (Exception e) {
log.error("AddBlock: status_confirmed_target_check_failed (login={}, blockchainName={}, blockNumber={})",
log.error("AddBlock: status_action_target_check_failed (login={}, blockchainName={}, blockNumber={})",
login, blockchainName, block.blockNumber, e);
return new AddBlockResult(WireCodes.Status.INTERNAL_ERROR, "internal_error", serverLastNum, serverLastHashHex);
}
@@ -653,7 +653,7 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
return null;
}
private String validateStatusConfirmedTarget(BchBlockEntry block) throws Exception {
private String validateStatusActionTarget(BchBlockEntry block) throws Exception {
if (!(block.body instanceof StatusActionBody statusBody)) return "bad_block_body";
String targetBch = statusBody.toBchName();
Integer targetBlockNumber = statusBody.toBlockGlobalNumber();
@@ -666,10 +666,31 @@ public final class Net_AddBlock_Handler implements JsonMessageHandler {
if (target == null || target.getBlockHash() == null || !Arrays.equals(target.getBlockHash(), targetHash)) {
return null;
}
if (target.getMsgType() != 5) {
return "status_confirmed_target_must_be_status_action";
int statusSubType = block.subType & 0xFFFF;
if (statusSubType == (MsgSubType.STATUS_CONFIRMED & 0xFFFF)) {
if (target.getMsgType() != 5) {
return "status_confirmed_target_must_be_status_action";
}
return null;
}
return null;
if (target.getMsgType() != 1) return "status_action_target_not_allowed";
int targetSubType = target.getMsgSubType();
if (statusSubType == (MsgSubType.STATUS_DONE_ONCE & 0xFFFF)
|| statusSubType == (MsgSubType.STATUS_LEARNED & 0xFFFF)) {
return targetSubType == (MsgSubType.TEXT_EXERCISE & 0xFFFF) ? null : "status_action_target_not_allowed";
}
if (statusSubType == (MsgSubType.STATUS_SERVICE_PASSED & 0xFFFF)) {
return targetSubType == (MsgSubType.TEXT_SERVICE & 0xFFFF) ? null : "status_action_target_not_allowed";
}
if (statusSubType == (MsgSubType.STATUS_INTERESTED & 0xFFFF)
|| statusSubType == (MsgSubType.STATUS_STARTED & 0xFFFF)
|| statusSubType == (MsgSubType.STATUS_IN_STUDY & 0xFFFF)
|| statusSubType == (MsgSubType.STATUS_ABANDONED & 0xFFFF)
|| statusSubType == (MsgSubType.STATUS_COMPLETED & 0xFFFF)) {
return targetSubType == (MsgSubType.TEXT_COURSE & 0xFFFF) ? null : "status_action_target_not_allowed";
}
return "status_action_target_not_allowed";
}
private ExistingChannelState loadExistingChannelState(String ownerBch, int rootBlockNumber) throws Exception {
@@ -0,0 +1,179 @@
package server.logic.ws_protocol.JSON.handlers.channels;
import blockchain.BchBlockEntry;
import blockchain.body.StatusActionBody;
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_GetPersonalDiary_Request;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.DbController;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
public class Net_GetPersonalDiary_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_GetPersonalDiary_Handler.class);
private static final String DIARY_CHANNEL_NAME = "diary";
private static final String DIARY_DISPLAY_NAME = "Личный дневник";
private static final String DIARY_DESCRIPTION = "История ваших действий по упражнениям, услугам и курсам";
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
Net_GetPersonalDiary_Request req = (Net_GetPersonalDiary_Request) baseRequest;
String requestedLogin = String.valueOf(req.getLogin() == null ? "" : req.getLogin()).trim();
if (requestedLogin.isBlank() && (ctx == null || ctx.getLogin() == null || ctx.getLogin().isBlank())) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "bad_fields", "Некорректные поля: login");
}
int limit = req.getLimit() == null ? 200 : 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 = DbController.getInstance().getConnection()) {
String viewerLogin = ctx != null ? String.valueOf(ctx.getLogin() == null ? "" : ctx.getLogin()).trim() : "";
String canonicalLogin = !viewerLogin.isBlank()
? viewerLogin
: ChannelsReadSupport.canonicalLogin(c, requestedLogin);
if (canonicalLogin == null || canonicalLogin.isBlank()) {
return NetExceptionResponseFactory.error(req, 404, "user_not_found", "Пользователь не найден");
}
if (!viewerLogin.isBlank() && !viewerLogin.equalsIgnoreCase(canonicalLogin)) {
return NetExceptionResponseFactory.error(req, 403, "forbidden", "Личный дневник доступен только владельцу");
}
String ownerBch = loadPrimaryBlockchainName(c, canonicalLogin);
if (ownerBch == null || ownerBch.isBlank()) {
return NetExceptionResponseFactory.error(req, 404, "blockchain_not_found", "Не найден blockchain пользователя");
}
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.setOwnerLogin(canonicalLogin);
channel.setOwnerBlockchainName(ownerBch);
channel.setChannelName(DIARY_CHANNEL_NAME);
channel.setDisplayName(DIARY_DISPLAY_NAME);
channel.setChannelDescription(DIARY_DESCRIPTION);
channel.setChannelTypeCode(900);
channel.setChannelTypeVersion(1);
Net_GetChannelMessages_Response.BlockRef rootRef = new Net_GetChannelMessages_Response.BlockRef();
rootRef.setBlockNumber(0);
rootRef.setBlockHash(ChannelsReadSupport.toHex(new byte[32]));
channel.setChannelRoot(rootRef);
resp.setChannel(channel);
resp.setMetaEvents(new ArrayList<>());
List<Net_GetChannelMessages_Response.MessageItem> items = loadDiaryItems(c, canonicalLogin, limit, asc);
resp.setMessages(items);
return resp;
} catch (Exception e) {
log.error("GetPersonalDiary failed", e);
return NetExceptionResponseFactory.error(req, WireCodes.Status.INTERNAL_ERROR, "internal_error", "Внутренняя ошибка сервера");
}
}
private String loadPrimaryBlockchainName(Connection c, String canonicalLogin) throws Exception {
try (PreparedStatement ps = c.prepareStatement("""
SELECT blockchain_name
FROM blockchain_state
WHERE login = ?
ORDER BY blockchain_name
LIMIT 1
""")) {
ps.setString(1, canonicalLogin);
try (ResultSet rs = ps.executeQuery()) {
return rs.next() ? rs.getString("blockchain_name") : null;
}
}
}
private List<Net_GetChannelMessages_Response.MessageItem> loadDiaryItems(Connection c, String canonicalLogin, int limit, boolean asc) throws Exception {
String order = asc ? "ASC" : "DESC";
List<Net_GetChannelMessages_Response.MessageItem> out = new ArrayList<>();
try (PreparedStatement ps = c.prepareStatement("""
SELECT login, bch_name, block_number, block_hash, block_bytes, msg_sub_type
FROM blocks
WHERE login = ? AND msg_type = ?
ORDER BY block_number
""" + order + " LIMIT ?")) {
ps.setString(1, canonicalLogin);
ps.setInt(2, ChannelsReadSupport.MSG_TYPE_STATUS_ACTION);
ps.setInt(3, limit);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
byte[] blockBytes = rs.getBytes("block_bytes");
BchBlockEntry entry = new BchBlockEntry(blockBytes);
if (!(entry.body instanceof StatusActionBody statusBody)) continue;
Net_GetChannelMessages_Response.MessageItem item = new Net_GetChannelMessages_Response.MessageItem();
Net_GetChannelMessages_Response.BlockRef ref = new Net_GetChannelMessages_Response.BlockRef();
ref.setBlockNumber(rs.getInt("block_number"));
ref.setBlockHash(ChannelsReadSupport.toHex(rs.getBytes("block_hash")));
item.setMessageRef(ref);
item.setMsgSubType(rs.getInt("msg_sub_type"));
item.setAuthorLogin(rs.getString("login"));
item.setAuthorBlockchainName(rs.getString("bch_name"));
item.setCreatedAtMs(entry.timestamp * 1000L);
item.setText(statusBody.message == null ? "" : statusBody.message);
item.setLikesCount(0);
item.setLikedByMe(false);
item.setRepliesCount(0);
item.setRatingsCount(0);
item.setVersionsTotal(1);
item.setVersions(new ArrayList<>());
item.setTargetBlockchainName(statusBody.toBchName());
item.setTargetBlockNumber(statusBody.toBlockGlobalNumber());
item.setTargetBlockHash(ChannelsReadSupport.toHex(statusBody.toBlockHashBytes()));
fillTargetDetails(c, item, statusBody.toBchName(), statusBody.toBlockGlobalNumber(), statusBody.toBlockHashBytes());
out.add(item);
}
}
}
return out;
}
private void fillTargetDetails(Connection c,
Net_GetChannelMessages_Response.MessageItem item,
String targetBch,
Integer targetBlockNumber,
byte[] targetHash) throws Exception {
if (targetBch == null || targetBch.isBlank() || targetBlockNumber == null || targetHash == null || targetHash.length != 32) {
return;
}
try (PreparedStatement ps = c.prepareStatement("""
SELECT login, bch_name, block_number, block_hash, block_bytes, msg_sub_type
FROM blocks
WHERE bch_name = ? AND block_number = ?
LIMIT 1
""")) {
ps.setString(1, targetBch);
ps.setInt(2, targetBlockNumber);
try (ResultSet rs = ps.executeQuery()) {
if (!rs.next()) return;
byte[] actualHash = rs.getBytes("block_hash");
if (actualHash == null || !java.util.Arrays.equals(actualHash, targetHash)) return;
item.setTargetMsgSubType(rs.getInt("msg_sub_type"));
item.setTargetAuthorLogin(rs.getString("login"));
item.setTargetAuthorBlockchainName(rs.getString("bch_name"));
ChannelsReadSupport.TextInfo textInfo = ChannelsReadSupport.parseTextAndTime(rs.getBytes("block_bytes"));
item.setTargetText(textInfo.text);
item.setTargetCreatedAtMs(textInfo.createdAtMs);
}
}
}
}
@@ -127,6 +127,11 @@ public class Net_GetChannelMessages_Response extends Net_Response {
private String targetBlockchainName;
private Integer targetBlockNumber;
private String targetBlockHash;
private Integer targetMsgSubType;
private String targetText;
private String targetAuthorLogin;
private String targetAuthorBlockchainName;
private Long targetCreatedAtMs;
private long createdAtMs;
private String text;
private int likesCount;
@@ -159,6 +164,21 @@ public class Net_GetChannelMessages_Response extends Net_Response {
public String getTargetBlockHash() { return targetBlockHash; }
public void setTargetBlockHash(String targetBlockHash) { this.targetBlockHash = targetBlockHash; }
public Integer getTargetMsgSubType() { return targetMsgSubType; }
public void setTargetMsgSubType(Integer targetMsgSubType) { this.targetMsgSubType = targetMsgSubType; }
public String getTargetText() { return targetText; }
public void setTargetText(String targetText) { this.targetText = targetText; }
public String getTargetAuthorLogin() { return targetAuthorLogin; }
public void setTargetAuthorLogin(String targetAuthorLogin) { this.targetAuthorLogin = targetAuthorLogin; }
public String getTargetAuthorBlockchainName() { return targetAuthorBlockchainName; }
public void setTargetAuthorBlockchainName(String targetAuthorBlockchainName) { this.targetAuthorBlockchainName = targetAuthorBlockchainName; }
public Long getTargetCreatedAtMs() { return targetCreatedAtMs; }
public void setTargetCreatedAtMs(Long targetCreatedAtMs) { this.targetCreatedAtMs = targetCreatedAtMs; }
public long getCreatedAtMs() { return createdAtMs; }
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
@@ -0,0 +1,18 @@
package server.logic.ws_protocol.JSON.handlers.channels.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_GetPersonalDiary_Request extends Net_Request {
private String login;
private Integer limit;
private String sort;
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; }
public String getSort() { return sort; }
public void setSort(String sort) { this.sort = sort; }
}