Удалить legacy-стек прямых сообщений

This commit is contained in:
AidarKC
2026-08-19 18:11:00 +04:00
parent 091291bfa2
commit fac166f186
13 changed files with 1 additions and 455 deletions
@@ -1,53 +0,0 @@
package shine.db.dao;
import shine.db.DbController;
import shine.db.entities.DirectMessageEntry;
import java.sql.Connection;
import java.sql.PreparedStatement;
public final class DirectMessagesDAO {
private static volatile DirectMessagesDAO instance;
private final DbController db = DbController.getInstance();
private DirectMessagesDAO() {}
public static DirectMessagesDAO getInstance() {
if (instance == null) {
synchronized (DirectMessagesDAO.class) {
if (instance == null) instance = new DirectMessagesDAO();
}
}
return instance;
}
public void insert(DirectMessageEntry entry) throws Exception {
try (Connection c = db.getConnection()) {
String sql = """
INSERT INTO direct_messages (
message_id, from_login, to_login, text, created_at_ms
) VALUES (?, ?, ?, ?, ?)
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, entry.getMessageId());
ps.setString(2, entry.getFromLogin());
ps.setString(3, entry.getToLogin());
ps.setString(4, entry.getText());
ps.setLong(5, entry.getCreatedAtMs());
ps.executeUpdate();
}
}
}
public boolean existsFromTo(String fromLogin, String toLogin) throws Exception {
try (Connection c = db.getConnection()) {
String sql = "SELECT 1 FROM direct_messages WHERE from_login = ? AND to_login = ? LIMIT 1";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, fromLogin);
ps.setString(2, toLogin);
return ps.executeQuery().next();
}
}
}
}
@@ -1,47 +0,0 @@
package shine.db.dao;
import shine.db.DbController;
import shine.db.entities.SignedDirectMessageHistoryEntry;
import java.sql.Connection;
import java.sql.PreparedStatement;
public final class SignedDirectMessagesHistoryDAO {
private static volatile SignedDirectMessagesHistoryDAO instance;
private final DbController db = DbController.getInstance();
private SignedDirectMessagesHistoryDAO() {}
public static SignedDirectMessagesHistoryDAO getInstance() {
if (instance == null) {
synchronized (SignedDirectMessagesHistoryDAO.class) {
if (instance == null) instance = new SignedDirectMessagesHistoryDAO();
}
}
return instance;
}
public void insert(SignedDirectMessageHistoryEntry e) throws Exception {
try (Connection c = db.getConnection()) {
String sql = """
INSERT INTO signed_direct_messages_history (
message_id, from_login, to_login, target_mode, target_session_id,
message_type, time_ms, nonce, raw_packet, created_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, e.getMessageId());
ps.setString(2, e.getFromLogin());
ps.setString(3, e.getToLogin());
ps.setInt(4, e.getTargetMode());
ps.setString(5, e.getTargetSessionId());
ps.setInt(6, e.getMessageType());
ps.setLong(7, e.getTimeMs());
ps.setLong(8, e.getNonce());
ps.setBytes(9, e.getRawPacket());
ps.setLong(10, e.getCreatedAtMs());
ps.executeUpdate();
}
}
}
}
@@ -1,51 +0,0 @@
package shine.db.dao;
import shine.db.DbController;
import java.sql.Connection;
import java.sql.PreparedStatement;
public final class SignedDmReplayDAO {
private static volatile SignedDmReplayDAO instance;
private final DbController db = DbController.getInstance();
private SignedDmReplayDAO() {}
public static SignedDmReplayDAO getInstance() {
if (instance == null) {
synchronized (SignedDmReplayDAO.class) {
if (instance == null) instance = new SignedDmReplayDAO();
}
}
return instance;
}
public boolean registerUnique(String fromLogin, long timeMs, long nonce, long nowMs) throws Exception {
cleanupExpired(nowMs - 15L * 60L * 1000L);
try (Connection c = db.getConnection()) {
String sql = """
INSERT INTO signed_direct_message_replay (
from_login, time_ms, nonce, created_at_ms
) VALUES (?, ?, ?, ?)
ON CONFLICT DO NOTHING
""";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setString(1, fromLogin);
ps.setLong(2, timeMs);
ps.setLong(3, nonce);
ps.setLong(4, nowMs);
return ps.executeUpdate() > 0;
}
}
}
public void cleanupExpired(long minCreatedAtMs) throws Exception {
try (Connection c = db.getConnection()) {
String sql = "DELETE FROM signed_direct_message_replay WHERE created_at_ms < ?";
try (PreparedStatement ps = c.prepareStatement(sql)) {
ps.setLong(1, minCreatedAtMs);
ps.executeUpdate();
}
}
}
}
@@ -1,24 +0,0 @@
package shine.db.entities;
public class DirectMessageEntry {
private String messageId;
private String fromLogin;
private String toLogin;
private String text;
private long createdAtMs;
public String getMessageId() { return messageId; }
public void setMessageId(String messageId) { this.messageId = messageId; }
public String getFromLogin() { return fromLogin; }
public void setFromLogin(String fromLogin) { this.fromLogin = fromLogin; }
public String getToLogin() { return toLogin; }
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
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; }
}
@@ -1,35 +0,0 @@
package shine.db.entities;
public class SignedDirectMessageHistoryEntry {
private String messageId;
private String fromLogin;
private String toLogin;
private int targetMode;
private String targetSessionId;
private int messageType;
private long timeMs;
private long nonce;
private byte[] rawPacket;
private long createdAtMs;
public String getMessageId() { return messageId; }
public void setMessageId(String messageId) { this.messageId = messageId; }
public String getFromLogin() { return fromLogin; }
public void setFromLogin(String fromLogin) { this.fromLogin = fromLogin; }
public String getToLogin() { return toLogin; }
public void setToLogin(String toLogin) { this.toLogin = toLogin; }
public int getTargetMode() { return targetMode; }
public void setTargetMode(int targetMode) { this.targetMode = targetMode; }
public String getTargetSessionId() { return targetSessionId; }
public void setTargetSessionId(String targetSessionId) { this.targetSessionId = targetSessionId; }
public int getMessageType() { return messageType; }
public void setMessageType(int messageType) { this.messageType = messageType; }
public long getTimeMs() { return timeMs; }
public void setTimeMs(long timeMs) { this.timeMs = timeMs; }
public long getNonce() { return nonce; }
public void setNonce(long nonce) { this.nonce = nonce; }
public byte[] getRawPacket() { return rawPacket; }
public void setRawPacket(byte[] rawPacket) { this.rawPacket = rawPacket; }
public long getCreatedAtMs() { return createdAtMs; }
public void setCreatedAtMs(long createdAtMs) { this.createdAtMs = createdAtMs; }
}
@@ -94,7 +94,6 @@ import server.logic.ws_protocol.JSON.messages.Net_DmSyncBatch_Handler;
import server.logic.ws_protocol.JSON.messages.Net_GetDirectMessages_Handler;
import server.logic.ws_protocol.JSON.messages.Net_SendSignal_Handler;
import server.logic.ws_protocol.JSON.messages.Net_ReceiveIncomingMessage_Handler;
import server.logic.ws_protocol.JSON.messages.Net_SendDirectMessage_Handler;
import server.logic.ws_protocol.JSON.messages.Net_SendMessagePair_Handler;
import server.logic.ws_protocol.JSON.messages.Net_SendTestWebPush_Handler;
import server.logic.ws_protocol.JSON.messages.Net_UpsertPushToken_Handler;
@@ -107,7 +106,6 @@ import server.logic.ws_protocol.JSON.messages.entyties.Net_DmSyncBatch_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_GetDirectMessages_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendSignal_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_ReceiveIncomingMessage_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendMessagePair_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendTestWebPush_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Request;
@@ -1,208 +0,0 @@
package server.logic.ws_protocol.JSON.messages;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
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.messages.entyties.Net_SendDirectMessage_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Response;
import server.logic.ws_protocol.JSON.push.WebPushSender;
import server.logic.ws_protocol.JSON.push.WsEventSender;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.ActiveSessionsDAO;
import shine.db.dao.DirectMessagesDAO;
import shine.db.dao.SignedDirectMessagesHistoryDAO;
import shine.db.dao.SignedDmReplayDAO;
import shine.db.dao.CurrentUsersDAO;
import shine.db.entities.ActiveSessionEntry;
import shine.db.entities.DirectMessageEntry;
import shine.db.entities.SignedDirectMessageHistoryEntry;
import shine.db.entities.CurrentUserEntry;
import utils.crypto.Ed25519Util;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
public class Net_SendDirectMessage_Handler implements JsonMessageHandler {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final long REPLAY_TTL_MS = 15L * 60L * 1000L;
private static final int MAX_MESSAGE_BYTES = 3000;
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
Net_SendDirectMessage_Request req = (Net_SendDirectMessage_Request) baseRequest;
if (req.getBlobB64() == null || req.getBlobB64().isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "blobB64 обязателен");
}
final byte[] raw;
final SignedDirectMessagePacket packet;
try {
raw = Base64.getDecoder().decode(req.getBlobB64().trim());
packet = SignedDirectMessagePacket.parse(raw, MAX_MESSAGE_BYTES);
} catch (IllegalArgumentException ex) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, ex.getMessage(), "Некорректный формат пакета");
}
CurrentUserEntry fromUser = CurrentUsersDAO.getInstance().getByLogin(packet.fromLogin);
CurrentUserEntry toUser = CurrentUsersDAO.getInstance().getByLogin(packet.toLogin);
if (fromUser == null || toUser == null) {
return NetExceptionResponseFactory.error(req, 404, "USER_NOT_FOUND", "from/to пользователь не найден");
}
byte[] publicKey32;
try {
publicKey32 = Ed25519Util.keyFromBase64(fromUser.getClientKey());
} catch (Exception e) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_DEVICE_KEY", "Некорректный clientKey отправителя");
}
if (!Ed25519Util.verify(packet.signedBody, packet.signature64, publicKey32)) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_SIGNATURE", "Подпись не прошла проверку");
}
long now = System.currentTimeMillis();
if (Math.abs(now - packet.timeMs) > REPLAY_TTL_MS) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "BAD_TIME_WINDOW", "Время сообщения вышло за окно 15 минут");
}
boolean replayOk = SignedDmReplayDAO.getInstance().registerUnique(packet.fromLogin, packet.timeMs, packet.nonce, now);
if (!replayOk) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "REPLAY", "Повторное сообщение заблокировано");
}
String messageId = NetIdGenerator.eventId("msg");
String textForUi = new String(packet.messageBytes, StandardCharsets.UTF_8);
DirectMessageEntry entry = new DirectMessageEntry();
entry.setMessageId(messageId);
entry.setFromLogin(packet.fromLogin);
entry.setToLogin(packet.toLogin);
entry.setText(textForUi);
entry.setCreatedAtMs(now);
DirectMessagesDAO.getInstance().insert(entry);
SignedDirectMessageHistoryEntry history = new SignedDirectMessageHistoryEntry();
history.setMessageId(messageId);
history.setFromLogin(packet.fromLogin);
history.setToLogin(packet.toLogin);
history.setTargetMode(packet.targetMode);
history.setTargetSessionId(packet.targetSessionId);
history.setMessageType(packet.messageType);
history.setTimeMs(packet.timeMs);
history.setNonce(packet.nonce);
history.setRawPacket(packet.rawPacket);
history.setCreatedAtMs(now);
SignedDirectMessagesHistoryDAO.getInstance().insert(history);
DeliveryResult delivery = deliver(packet, req.getBlobB64().trim(), messageId, now);
Net_SendDirectMessage_Response resp = new Net_SendDirectMessage_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setMessageId(messageId);
resp.setDeliveredWsSessions(delivery.wsDelivered);
resp.setDeliveredWebPushSessions(delivery.webPushDelivered);
resp.setSessionNotFound(delivery.sessionNotFound);
return resp;
}
private DeliveryResult deliver(SignedDirectMessagePacket packet, String blobB64, String messageId, long createdAtMs) throws Exception {
DeliveryResult result = new DeliveryResult();
Set<String> selectedSessionIds = new HashSet<>();
if (packet.targetMode == SignedDirectMessagePacket.TARGET_ONE_SESSION) {
ActiveSessionEntry byId = ActiveSessionsDAO.getInstance().getBySessionId(packet.targetSessionId);
if (byId == null || !packet.toLogin.equalsIgnoreCase(byId.getLogin())) {
result.sessionNotFound = true;
return result;
}
selectedSessionIds.add(byId.getSessionId());
deliverToSession(packet, blobB64, messageId, createdAtMs, byId.getSessionId(), result);
return result;
}
List<ActiveSessionEntry> sessions = ActiveSessionsDAO.getInstance().getByLogin(packet.toLogin);
for (ActiveSessionEntry s : sessions) {
selectedSessionIds.add(s.getSessionId());
deliverToSession(packet, blobB64, messageId, createdAtMs, s.getSessionId(), result);
}
return result;
}
private void deliverToSession(
SignedDirectMessagePacket packet,
String blobB64,
String messageId,
long createdAtMs,
String sessionId,
DeliveryResult result
) {
ConnectionContext targetCtx = ActiveConnectionsRegistry.getInstance().getBySessionId(sessionId);
boolean wsDelivered = false;
if (targetCtx != null) {
String eventId = NetIdGenerator.eventId("evt");
CompletableFuture<Boolean> waiter = DeliveryTracker.getInstance().register(eventId);
ObjectNode payload = MAPPER.createObjectNode();
payload.put("eventId", eventId);
payload.put("messageId", messageId);
payload.put("fromLogin", packet.fromLogin);
payload.put("toLogin", packet.toLogin);
payload.put("blobB64", blobB64);
payload.put("text", new String(packet.messageBytes, StandardCharsets.UTF_8));
payload.put("timeMs", createdAtMs);
boolean sent = WsEventSender.sendEvent(targetCtx, "IncomingDirectMessage", eventId, payload);
if (sent) {
try {
wsDelivered = waiter.get(1200, TimeUnit.MILLISECONDS);
} catch (Exception ignored) {
wsDelivered = false;
}
}
DeliveryTracker.getInstance().remove(eventId);
}
if (wsDelivered) {
result.wsDelivered++;
return;
}
try {
ActiveSessionEntry targetSession = ActiveSessionsDAO.getInstance().getBySessionId(sessionId);
if (targetSession == null) return;
if (isBlank(targetSession.getPushEndpoint()) || isBlank(targetSession.getPushP256dhKey()) || isBlank(targetSession.getPushAuthKey())) {
return;
}
boolean pushed = WebPushSender.sendBase64Payload(
targetSession.getPushEndpoint(),
targetSession.getPushP256dhKey(),
targetSession.getPushAuthKey(),
blobB64
);
if (pushed) result.webPushDelivered++;
} catch (Exception ignored) {
// ignore per-session push errors
}
}
private boolean isBlank(String s) {
return s == null || s.isBlank();
}
private static final class DeliveryResult {
int wsDelivered;
int webPushDelivered;
boolean sessionNotFound;
}
}
@@ -1,10 +0,0 @@
package server.logic.ws_protocol.JSON.messages.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_SendDirectMessage_Request extends Net_Request {
private String blobB64;
public String getBlobB64() { return blobB64; }
public void setBlobB64(String blobB64) { this.blobB64 = blobB64; }
}
@@ -1,19 +0,0 @@
package server.logic.ws_protocol.JSON.messages.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
public class Net_SendDirectMessage_Response extends Net_Response {
private String messageId;
private int deliveredWsSessions;
private int deliveredWebPushSessions;
private boolean sessionNotFound;
public String getMessageId() { return messageId; }
public void setMessageId(String messageId) { this.messageId = messageId; }
public int getDeliveredWsSessions() { return deliveredWsSessions; }
public void setDeliveredWsSessions(int deliveredWsSessions) { this.deliveredWsSessions = deliveredWsSessions; }
public int getDeliveredWebPushSessions() { return deliveredWebPushSessions; }
public void setDeliveredWebPushSessions(int deliveredWebPushSessions) { this.deliveredWebPushSessions = deliveredWebPushSessions; }
public boolean isSessionNotFound() { return sessionNotFound; }
public void setSessionNotFound(boolean sessionNotFound) { this.sessionNotFound = sessionNotFound; }
}
+1 -1
View File
@@ -1,2 +1,2 @@
client.version=1.7.0
server.version=1.6.0
server.version=1.6.1
-1
View File
@@ -71,7 +71,6 @@
## Важные замечания
- `ReceiveOutcomingMessage` сейчас зарегистрирован как алиас того же handler/request-класса, что и `SendMessagePair`.
- Legacy-операция `SendDirectMessage` больше не зарегистрирована и не должна использоваться для DM v1.
- Отдельных HTTP endpoints для DM-файлов сейчас нет.
- Классы `Net_MarkChannelMessagesSeen_*` существуют в коде, но операция `MarkChannelMessagesSeen` не зарегистрирована в `JsonHandlerRegistry`, поэтому в публичный список API не входит.
- HTTP debug endpoints из `src/main/java/server/debug/` не входят в этот индекс WebSocket `op`; они описаны отдельно в `13_HTTP_Debug_API.md`.
@@ -9,7 +9,6 @@
Важно:
- legacy-операция `SendDirectMessage` отключена и не зарегистрирована в публичном API;
- для DM v1 нужно использовать `SendMessagePair`, `ReceiveOutcomingMessage`, `ReceiveIncomingMessage`, `DeleteMessage`, `DeleteConversation`, `GetDirectMessages`;
- `DmSyncBatch` предназначен для межсерверной догоняющей синхронизации, не для обычного клиентского UI.
@@ -334,8 +334,6 @@
### 8.2. Новые методы, которые нужны
Отдельный legacy-метод `SendDirectMessage` в DM v1 не используется и должен оставаться отключённым, чтобы не было параллельного старого стека доставки.
## 9. Правила валидации и применения
### 9.1. Общее правило по ревизиям
@@ -653,7 +651,6 @@ UI-следствие для клиента:
- межсерверная маршрутизация DM должна идти через `access_servers`;
- сервер должен добирать отсутствующих пользователей из Solana PDA до проверки подписи DM;
- при выборе актуальной версии должен учитываться `reencryptedAtMs`, если `revisionTimeMs` совпадает;
- legacy `SendDirectMessage` должен быть отключён;
- логика должна быть безопасна для нескольких серверов у каждой стороны.
## 14. Что в v1 пока не входит