Add WS push events, PWA/FCM scaffolding, and direct messaging MVP

This commit is contained in:
ai5590
2026-04-04 18:10:25 +03:00
parent cf5460c5c7
commit 32c046233b
40 changed files with 1300 additions and 114 deletions
@@ -51,6 +51,16 @@ import server.logic.ws_protocol.JSON.handlers.channels.Net_ListSubscriptionsFeed
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetChannelMessages_Request;
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_GetMessageThread_Request;
import server.logic.ws_protocol.JSON.handlers.channels.entyties.Net_ListSubscriptionsFeed_Request;
import server.logic.ws_protocol.JSON.handlers.connections.Net_GetUserConnectionsGraph_Handler;
import server.logic.ws_protocol.JSON.handlers.connections.Net_ListContacts_Handler;
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetUserConnectionsGraph_Request;
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Request;
import server.logic.ws_protocol.JSON.messages.Net_AckIncomingMessage_Handler;
import server.logic.ws_protocol.JSON.messages.Net_SendDirectMessage_Handler;
import server.logic.ws_protocol.JSON.messages.Net_UpsertPushToken_Handler;
import server.logic.ws_protocol.JSON.messages.entyties.Net_AckIncomingMessage_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_SendDirectMessage_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Request;
// --- NEW: Ping ---
import server.logic.ws_protocol.JSON.handlers.system.Net_GetServerInfo_Handler;
@@ -96,6 +106,13 @@ public final class JsonHandlerRegistry {
Map.entry("ListSubscriptionsFeed", new Net_ListSubscriptionsFeed_Handler()),
Map.entry("GetChannelMessages", new Net_GetChannelMessages_Handler()),
Map.entry("GetMessageThread", new Net_GetMessageThread_Handler()),
Map.entry("ListContacts", new Net_ListContacts_Handler()),
Map.entry("GetUserConnectionsGraph", new Net_GetUserConnectionsGraph_Handler()),
// --- direct messages / push ---
Map.entry("UpsertPushToken", new Net_UpsertPushToken_Handler()),
Map.entry("SendDirectMessage", new Net_SendDirectMessage_Handler()),
Map.entry("AckIncomingMessage", new Net_AckIncomingMessage_Handler()),
// --- system ---
Map.entry("Ping", new Net_Ping_Handler()),
@@ -134,6 +151,13 @@ public final class JsonHandlerRegistry {
Map.entry("ListSubscriptionsFeed", Net_ListSubscriptionsFeed_Request.class),
Map.entry("GetChannelMessages", Net_GetChannelMessages_Request.class),
Map.entry("GetMessageThread", Net_GetMessageThread_Request.class),
Map.entry("ListContacts", Net_ListContacts_Request.class),
Map.entry("GetUserConnectionsGraph", Net_GetUserConnectionsGraph_Request.class),
// --- direct messages / push ---
Map.entry("UpsertPushToken", Net_UpsertPushToken_Request.class),
Map.entry("SendDirectMessage", Net_SendDirectMessage_Request.class),
Map.entry("AckIncomingMessage", Net_AckIncomingMessage_Request.class),
// --- system ---
Map.entry("Ping", Net_Ping_Request.class),
@@ -1,5 +1,7 @@
package server.logic.ws_protocol.JSON.handlers.auth;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.ActiveConnectionsRegistry;
@@ -10,6 +12,8 @@ import server.logic.ws_protocol.JSON.handlers.JsonMessageHandler;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Request;
import server.logic.ws_protocol.JSON.handlers.auth.entyties.Net_CloseActiveSession_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.JSON.push.WsEventSender;
import server.logic.ws_protocol.JSON.utils.NetIdGenerator;
import server.logic.ws_protocol.WireCodes;
import server.ws.WsConnectionUtils;
import shine.db.dao.ActiveSessionsDAO;
@@ -32,6 +36,7 @@ import java.sql.SQLException;
public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
private static final Logger log = LoggerFactory.getLogger(Net_CloseActiveSession_Handler.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public Net_Response handle(Net_Request baseReq, ConnectionContext ctx) throws Exception {
@@ -122,6 +127,14 @@ public class Net_CloseActiveSession_Handler implements JsonMessageHandler {
if (ctxToClose == null) return;
String eventId = NetIdGenerator.eventId("evt");
ObjectNode payload = MAPPER.createObjectNode();
payload.put("eventId", eventId);
payload.put("sessionId", targetSessionId);
payload.put("reason", "closed_from_another_device");
payload.put("timeMs", System.currentTimeMillis());
WsEventSender.sendEvent(ctxToClose, "SessionRevoked", eventId, payload);
if (isCurrentSession && ctxToClose == currentCtx) {
new Thread(() -> {
try { Thread.sleep(50); } catch (InterruptedException ignored) {}
@@ -0,0 +1,40 @@
package server.logic.ws_protocol.JSON.handlers.connections;
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.connections.entyties.Net_GetUserConnectionsGraph_Request;
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_GetUserConnectionsGraph_Response;
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.util.List;
public class Net_GetUserConnectionsGraph_Handler implements JsonMessageHandler {
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
Net_GetUserConnectionsGraph_Request req = (Net_GetUserConnectionsGraph_Request) baseRequest;
if (ctx == null || !ctx.isAuthenticatedUser()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
}
String login = (req.getLogin() == null || req.getLogin().isBlank()) ? ctx.getLogin() : req.getLogin().trim();
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
List<String> out = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, login, MsgSubType.CONNECTION_FRIEND);
List<String> in = ConnectionsStateDAO.getInstance().listIncomingByRelTypeCanonical(c, login, MsgSubType.CONNECTION_FRIEND);
Net_GetUserConnectionsGraph_Response resp = new Net_GetUserConnectionsGraph_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setLogin(login);
resp.setOutFriends(out);
resp.setInFriends(in);
return resp;
}
}
}
@@ -0,0 +1,36 @@
package server.logic.ws_protocol.JSON.handlers.connections;
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.connections.entyties.Net_ListContacts_Request;
import server.logic.ws_protocol.JSON.handlers.connections.entyties.Net_ListContacts_Response;
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.util.List;
public class Net_ListContacts_Handler implements JsonMessageHandler {
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
Net_ListContacts_Request req = (Net_ListContacts_Request) baseRequest;
if (ctx == null || !ctx.isAuthenticatedUser()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
}
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
List<String> contacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, ctx.getLogin(), MsgSubType.CONNECTION_CONTACT);
Net_ListContacts_Response resp = new Net_ListContacts_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setLogin(ctx.getLogin());
resp.setContacts(contacts);
return resp;
}
}
}
@@ -0,0 +1,10 @@
package server.logic.ws_protocol.JSON.handlers.connections.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_GetUserConnectionsGraph_Request extends Net_Request {
private String login;
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
}
@@ -0,0 +1,19 @@
package server.logic.ws_protocol.JSON.handlers.connections.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import java.util.ArrayList;
import java.util.List;
public class Net_GetUserConnectionsGraph_Response extends Net_Response {
private String login;
private List<String> outFriends = new ArrayList<>();
private List<String> inFriends = new ArrayList<>();
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
public List<String> getOutFriends() { return outFriends; }
public void setOutFriends(List<String> outFriends) { this.outFriends = outFriends; }
public List<String> getInFriends() { return inFriends; }
public void setInFriends(List<String> inFriends) { this.inFriends = inFriends; }
}
@@ -0,0 +1,6 @@
package server.logic.ws_protocol.JSON.handlers.connections.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_ListContacts_Request extends Net_Request {
}
@@ -0,0 +1,16 @@
package server.logic.ws_protocol.JSON.handlers.connections.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
import java.util.ArrayList;
import java.util.List;
public class Net_ListContacts_Response extends Net_Response {
private String login;
private List<String> contacts = new ArrayList<>();
public String getLogin() { return login; }
public void setLogin(String login) { this.login = login; }
public List<String> getContacts() { return contacts; }
public void setContacts(List<String> contacts) { this.contacts = contacts; }
}
@@ -0,0 +1,34 @@
package server.logic.ws_protocol.JSON.messages;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public final class DeliveryTracker {
private static final DeliveryTracker INSTANCE = new DeliveryTracker();
public static DeliveryTracker getInstance() { return INSTANCE; }
private final ConcurrentHashMap<String, CompletableFuture<Boolean>> waiters = new ConcurrentHashMap<>();
private DeliveryTracker() {}
public CompletableFuture<Boolean> register(String eventId) {
CompletableFuture<Boolean> f = new CompletableFuture<>();
waiters.put(eventId, f);
return f;
}
public void ack(String eventId) {
CompletableFuture<Boolean> f = waiters.remove(eventId);
if (f != null) f.complete(true);
}
public void fail(String eventId) {
CompletableFuture<Boolean> f = waiters.remove(eventId);
if (f != null) f.complete(false);
}
public void remove(String eventId) {
waiters.remove(eventId);
}
}
@@ -0,0 +1,29 @@
package server.logic.ws_protocol.JSON.messages;
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_AckIncomingMessage_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_AckIncomingMessage_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
public class Net_AckIncomingMessage_Handler implements JsonMessageHandler {
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) {
Net_AckIncomingMessage_Request req = (Net_AckIncomingMessage_Request) baseRequest;
if (ctx == null || !ctx.isAuthenticatedUser()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
}
if (req.getEventId() != null && !req.getEventId().isBlank()) {
DeliveryTracker.getInstance().ack(req.getEventId());
}
Net_AckIncomingMessage_Response resp = new Net_AckIncomingMessage_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
return resp;
}
}
@@ -0,0 +1,141 @@
package server.logic.ws_protocol.JSON.messages;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.FcmPushSender;
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.MsgSubType;
import shine.db.dao.ConnectionsStateDAO;
import shine.db.dao.DirectMessagesDAO;
import shine.db.dao.PushTokensDAO;
import shine.db.entities.DirectMessageEntry;
import shine.db.entities.PushTokenEntry;
import java.sql.Connection;
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 Logger log = LoggerFactory.getLogger(Net_SendDirectMessage_Handler.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
Net_SendDirectMessage_Request req = (Net_SendDirectMessage_Request) baseRequest;
if (ctx == null || !ctx.isAuthenticatedUser()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
}
if (req.getToLogin() == null || req.getToLogin().isBlank() || req.getText() == null || req.getText().isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "toLogin и text обязательны");
}
String from = ctx.getLogin();
String to = req.getToLogin().trim();
String text = req.getText().trim();
if (!canSend(from, to)) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NO_PERMISSION", "Можно писать только контактам или тем, кто уже писал вам");
}
String messageId = NetIdGenerator.eventId("msg");
DirectMessageEntry entry = new DirectMessageEntry();
entry.setMessageId(messageId);
entry.setFromLogin(from);
entry.setToLogin(to);
entry.setText(text);
entry.setCreatedAtMs(System.currentTimeMillis());
DirectMessagesDAO.getInstance().insert(entry);
Set<ConnectionContext> activeSessions = ActiveConnectionsRegistry.getInstance().getByLogin(to);
List<PushTokenEntry> tokens = PushTokensDAO.getInstance().listByLogin(to);
int wsDelivered = 0;
int fcmDelivered = 0;
Set<String> activeSessionIds = new HashSet<>();
for (ConnectionContext targetCtx : activeSessions) {
activeSessionIds.add(targetCtx.getSessionId());
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", from);
payload.put("toLogin", to);
payload.put("text", text);
payload.put("timeMs", entry.getCreatedAtMs());
boolean sent = WsEventSender.sendEvent(targetCtx, "IncomingDirectMessage", eventId, payload);
boolean acked = false;
if (sent) {
try {
acked = waiter.get(1200, TimeUnit.MILLISECONDS);
} catch (Exception ignored) {
acked = false;
}
}
DeliveryTracker.getInstance().remove(eventId);
if (acked) {
wsDelivered++;
continue;
}
for (PushTokenEntry token : tokens) {
if (!targetCtx.getSessionId().equals(token.getSessionId())) continue;
boolean pushed = FcmPushSender.sendNotification(token.getToken(), "Новое сообщение", text, messageId);
if (pushed) {
fcmDelivered++;
break;
}
}
}
for (PushTokenEntry token : tokens) {
if (activeSessionIds.contains(token.getSessionId())) continue;
boolean pushed = FcmPushSender.sendNotification(token.getToken(), "Новое сообщение", text, messageId);
if (pushed) fcmDelivered++;
}
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(wsDelivered);
resp.setDeliveredFcmSessions(fcmDelivered);
return resp;
}
private boolean canSend(String from, String to) throws Exception {
if (from.equalsIgnoreCase(to)) return true;
try (Connection c = shine.db.SqliteDbController.getInstance().getConnection()) {
List<String> contacts = ConnectionsStateDAO.getInstance().listOutgoingByRelTypeCanonical(c, from, MsgSubType.CONNECTION_CONTACT);
if (contacts.stream().anyMatch(v -> v.equalsIgnoreCase(to))) {
return true;
}
if (DirectMessagesDAO.getInstance().existsFromTo(to, from)) {
return true;
}
return false;
} catch (Exception e) {
log.warn("canSend fallback false due error", e);
return false;
}
}
}
@@ -0,0 +1,44 @@
package server.logic.ws_protocol.JSON.messages;
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_UpsertPushToken_Request;
import server.logic.ws_protocol.JSON.messages.entyties.Net_UpsertPushToken_Response;
import server.logic.ws_protocol.JSON.utils.NetExceptionResponseFactory;
import server.logic.ws_protocol.WireCodes;
import shine.db.dao.PushTokensDAO;
import shine.db.entities.PushTokenEntry;
public class Net_UpsertPushToken_Handler implements JsonMessageHandler {
@Override
public Net_Response handle(Net_Request baseRequest, ConnectionContext ctx) throws Exception {
Net_UpsertPushToken_Request req = (Net_UpsertPushToken_Request) baseRequest;
if (ctx == null || !ctx.isAuthenticatedUser()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.UNVERIFIED, "NOT_AUTHENTICATED", "Требуется авторизация");
}
if (req.getTokenId() == null || req.getTokenId().isBlank() || req.getToken() == null || req.getToken().isBlank()) {
return NetExceptionResponseFactory.error(req, WireCodes.Status.BAD_REQUEST, "BAD_FIELDS", "tokenId и token обязательны");
}
PushTokenEntry e = new PushTokenEntry();
e.setTokenId(req.getTokenId().trim());
e.setLogin(ctx.getLogin());
e.setSessionId((req.getSessionId() == null || req.getSessionId().isBlank()) ? ctx.getSessionId() : req.getSessionId().trim());
e.setProvider(req.getProvider() == null || req.getProvider().isBlank() ? "fcm" : req.getProvider().trim());
e.setToken(req.getToken().trim());
e.setPlatform(req.getPlatform());
e.setUserAgent(req.getUserAgent());
e.setUpdatedAtMs(System.currentTimeMillis());
PushTokensDAO.getInstance().upsert(e);
Net_UpsertPushToken_Response resp = new Net_UpsertPushToken_Response();
resp.setOp(req.getOp());
resp.setRequestId(req.getRequestId());
resp.setStatus(WireCodes.Status.OK);
resp.setTokenId(e.getTokenId());
resp.setUpdatedAtMs(e.getUpdatedAtMs());
return resp;
}
}
@@ -0,0 +1,13 @@
package server.logic.ws_protocol.JSON.messages.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_AckIncomingMessage_Request extends Net_Request {
private String eventId;
private String messageId;
public String getEventId() { return eventId; }
public void setEventId(String eventId) { this.eventId = eventId; }
public String getMessageId() { return messageId; }
public void setMessageId(String messageId) { this.messageId = messageId; }
}
@@ -0,0 +1,6 @@
package server.logic.ws_protocol.JSON.messages.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
public class Net_AckIncomingMessage_Response extends Net_Response {
}
@@ -0,0 +1,13 @@
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 toLogin;
private String text;
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; }
}
@@ -0,0 +1,16 @@
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 deliveredFcmSessions;
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 getDeliveredFcmSessions() { return deliveredFcmSessions; }
public void setDeliveredFcmSessions(int deliveredFcmSessions) { this.deliveredFcmSessions = deliveredFcmSessions; }
}
@@ -0,0 +1,25 @@
package server.logic.ws_protocol.JSON.messages.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Request;
public class Net_UpsertPushToken_Request extends Net_Request {
private String tokenId;
private String sessionId;
private String provider;
private String token;
private String platform;
private String userAgent;
public String getTokenId() { return tokenId; }
public void setTokenId(String tokenId) { this.tokenId = tokenId; }
public String getSessionId() { return sessionId; }
public void setSessionId(String sessionId) { this.sessionId = sessionId; }
public String getProvider() { return provider; }
public void setProvider(String provider) { this.provider = provider; }
public String getToken() { return token; }
public void setToken(String token) { this.token = token; }
public String getPlatform() { return platform; }
public void setPlatform(String platform) { this.platform = platform; }
public String getUserAgent() { return userAgent; }
public void setUserAgent(String userAgent) { this.userAgent = userAgent; }
}
@@ -0,0 +1,13 @@
package server.logic.ws_protocol.JSON.messages.entyties;
import server.logic.ws_protocol.JSON.entyties.Net_Response;
public class Net_UpsertPushToken_Response extends Net_Response {
private String tokenId;
private long updatedAtMs;
public String getTokenId() { return tokenId; }
public void setTokenId(String tokenId) { this.tokenId = tokenId; }
public long getUpdatedAtMs() { return updatedAtMs; }
public void setUpdatedAtMs(long updatedAtMs) { this.updatedAtMs = updatedAtMs; }
}
@@ -0,0 +1,52 @@
package server.logic.ws_protocol.JSON.push;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import utils.config.AppConfig;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
public final class FcmPushSender {
private static final Logger log = LoggerFactory.getLogger(FcmPushSender.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final HttpClient HTTP = HttpClient.newHttpClient();
private FcmPushSender() {}
public static boolean sendNotification(String token, String title, String body, String messageId) {
try {
String serverKey = AppConfig.getInstance().getStringOrEmpty("fcm.server.key");
if (serverKey.isBlank()) {
log.warn("fcm.server.key is empty, skip FCM send");
return false;
}
ObjectNode root = MAPPER.createObjectNode();
root.put("to", token);
ObjectNode notif = root.putObject("notification");
notif.put("title", title);
notif.put("body", body);
ObjectNode data = root.putObject("data");
data.put("messageId", messageId);
HttpRequest req = HttpRequest.newBuilder(URI.create("https://fcm.googleapis.com/fcm/send"))
.timeout(Duration.ofSeconds(5))
.header("Authorization", "key=" + serverKey)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(root.toString()))
.build();
HttpResponse<String> resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
return resp.statusCode() >= 200 && resp.statusCode() < 300;
} catch (Exception e) {
log.warn("FCM send failed", e);
return false;
}
}
}
@@ -0,0 +1,35 @@
package server.logic.ws_protocol.JSON.push;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.eclipse.jetty.websocket.api.Session;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import server.logic.ws_protocol.JSON.ConnectionContext;
public final class WsEventSender {
private static final Logger log = LoggerFactory.getLogger(WsEventSender.class);
private static final ObjectMapper MAPPER = new ObjectMapper();
private WsEventSender() {}
public static boolean sendEvent(ConnectionContext ctx, String op, String eventId, ObjectNode payload) {
if (ctx == null) return false;
Session session = ctx.getWsSession();
if (session == null || !session.isOpen()) return false;
try {
ObjectNode root = MAPPER.createObjectNode();
root.put("op", op);
root.put("requestId", eventId);
root.put("status", 200);
root.put("ok", true);
root.put("event", true);
root.set("payload", payload == null ? MAPPER.createObjectNode() : payload);
session.getRemote().sendString(root.toString());
return true;
} catch (Exception e) {
log.warn("Failed to send ws event op={} sessionId={}", op, ctx.getSessionId(), e);
return false;
}
}
}
@@ -0,0 +1,28 @@
package server.logic.ws_protocol.JSON.utils;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ThreadLocalRandom;
public final class NetIdGenerator {
private static final String ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss-SSS");
private NetIdGenerator() {}
public static String eventId(String prefix) {
LocalDateTime now = LocalDateTime.now(ZoneOffset.UTC);
return (prefix == null || prefix.isBlank() ? "evt" : prefix)
+ "-" + now.format(FMT)
+ "-" + randomSuffix(10);
}
private static String randomSuffix(int len) {
StringBuilder sb = new StringBuilder(len);
for (int i = 0; i < len; i++) {
sb.append(ALPHABET.charAt(ThreadLocalRandom.current().nextInt(ALPHABET.length())));
}
return sb.toString();
}
}