Refactor EventPublisher and ChatService: replace UUID-based keys with generic unique string keys, add session cleanup callbacks, and update related logic for better extensibility and reusability.
This commit is contained in:
parent
f7edde65d6
commit
b71a2f19b8
|
|
@ -0,0 +1,52 @@
|
||||||
|
package com.alttd.altitudeweb.controllers.event;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.services.chat.to_server.ServerMessageService;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||||
|
import org.springframework.security.core.token.Token;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/chat/send")
|
||||||
|
public class CommandsToServerController {
|
||||||
|
|
||||||
|
private final ServerMessageService serverMessageService;
|
||||||
|
@Value("${altitudeweb.token}")
|
||||||
|
private String validToken;
|
||||||
|
|
||||||
|
@GetMapping(path = "/subscribe/{server}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
|
public SseEmitter subscribe(@AuthenticationPrincipal Token token,
|
||||||
|
HttpServletResponse response, @PathVariable String server) {
|
||||||
|
if (validToken == null || validToken.equals("invalid-token")) {
|
||||||
|
log.error("Invalid token in config");
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
|
||||||
|
}
|
||||||
|
if (token == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token.getKey().equals(validToken)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
response.setHeader("X-Accel-Buffering", "no"); // disables nginx buffering if present
|
||||||
|
response.setHeader("Cache-Control", "no-cache");
|
||||||
|
|
||||||
|
return serverMessageService.subscribe(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,8 @@ package com.alttd.altitudeweb.controllers.event;
|
||||||
import com.alttd.altitudeweb.controllers.chat.ChatMessageMapper;
|
import com.alttd.altitudeweb.controllers.chat.ChatMessageMapper;
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
import com.alttd.altitudeweb.database.chat.ChatLogMapper;
|
import com.alttd.altitudeweb.database.chat.ChatLogMapper;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.chat_session.ChatSession;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.chat_session.ChatSessionMapper;
|
||||||
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
||||||
import com.alttd.altitudeweb.services.chat.ChatService;
|
import com.alttd.altitudeweb.services.chat.ChatService;
|
||||||
import com.alttd.altitudeweb.services.chat.event_publisher.EventPublisher;
|
import com.alttd.altitudeweb.services.chat.event_publisher.EventPublisher;
|
||||||
|
|
@ -37,7 +39,6 @@ public class EventController {
|
||||||
|
|
||||||
private final static Duration MESSAGE_HISTORY_DURATION = Duration.ofHours(1);
|
private final static Duration MESSAGE_HISTORY_DURATION = Duration.ofHours(1);
|
||||||
private final ChatService chatService;
|
private final ChatService chatService;
|
||||||
private final EventPublisher eventPublisher;
|
|
||||||
|
|
||||||
@GetMapping(path = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
@GetMapping(path = "/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||||
public SseEmitter subscribe(@AuthenticationPrincipal Jwt jwt,
|
public SseEmitter subscribe(@AuthenticationPrincipal Jwt jwt,
|
||||||
|
|
@ -68,7 +69,8 @@ public class EventController {
|
||||||
.collect(Collectors.joining(",", "[", "]"));
|
.collect(Collectors.joining(",", "[", "]"));
|
||||||
|
|
||||||
Integer partyId = getPartyId(subject);
|
Integer partyId = getPartyId(subject);
|
||||||
return eventPublisher.subscribe(new EventUser(subject, authorities, partyId), json);
|
EventUser eventUser = new EventUser(subject, authorities, partyId);
|
||||||
|
return chatService.subscribe(eventUser, json);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Integer getPartyId(UUID subject) {
|
private Integer getPartyId(UUID subject) {
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,12 @@ import com.alttd.altitudeweb.controllers.chat.ChatMessage;
|
||||||
import com.alttd.altitudeweb.controllers.chat.ChatMessageMapper;
|
import com.alttd.altitudeweb.controllers.chat.ChatMessageMapper;
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
import com.alttd.altitudeweb.database.chat.ChatLogMapper;
|
import com.alttd.altitudeweb.database.chat.ChatLogMapper;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.chat_session.ChatSession;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.chat_session.ChatSessionMapper;
|
||||||
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
||||||
import com.alttd.altitudeweb.services.chat.event_publisher.EventPublisher;
|
import com.alttd.altitudeweb.services.chat.event_publisher.EventPublisher;
|
||||||
import com.alttd.altitudeweb.services.chat.event_publisher.EventUser;
|
import com.alttd.altitudeweb.services.chat.event_publisher.EventUser;
|
||||||
|
import com.alttd.altitudeweb.services.chat.event_publisher.MessageForUser;
|
||||||
import com.alttd.altitudeweb.setup.Connection;
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
@ -15,6 +18,7 @@ import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||||
import org.springframework.context.event.EventListener;
|
import org.springframework.context.event.EventListener;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
|
|
@ -27,10 +31,12 @@ import java.util.stream.Collectors;
|
||||||
public class ChatService {
|
public class ChatService {
|
||||||
|
|
||||||
private static final Duration MAX_AGE = Duration.ofHours(1);
|
private static final Duration MAX_AGE = Duration.ofHours(1);
|
||||||
|
private final EventPublisher eventPublisher;
|
||||||
|
private final NavigableMap<Instant, ChatMessage> chatMessages = new TreeMap<>();
|
||||||
|
private final Map<String, EventUser> eventUserMap = new HashMap<>();
|
||||||
|
|
||||||
@Value("${chat.allowed-servers}")
|
@Value("${chat.allowed-servers}")
|
||||||
private String[] allowedServers;
|
private String[] allowedServers;
|
||||||
private final NavigableMap<Instant, ChatMessage> chatMessages = new TreeMap<>();
|
|
||||||
private final EventPublisher eventPublisher;
|
|
||||||
private long lastMillis = 0;
|
private long lastMillis = 0;
|
||||||
|
|
||||||
@Scheduled(cron = "0 * * * * *")
|
@Scheduled(cron = "0 * * * * *")
|
||||||
|
|
@ -61,6 +67,39 @@ public class ChatService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SseEmitter subscribe(EventUser eventUser, String json) {
|
||||||
|
String key = eventUser.uuid().toString();
|
||||||
|
eventUserMap.put(key, eventUser);
|
||||||
|
return eventPublisher.subscribe(key, json, this::handleSessionEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSessionEnd(String key, Instant sessionStart, Instant sessionEnd) {
|
||||||
|
EventUser eventUser = eventUserMap.get(key);
|
||||||
|
if (eventUser == null) {
|
||||||
|
log.error("Failed to find event user for key {}", key);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ChatSession chatSession = ChatSession.builder()
|
||||||
|
.uuid(eventUser.uuid())
|
||||||
|
.session_start(sessionStart)
|
||||||
|
.session_end(sessionEnd)
|
||||||
|
.build();
|
||||||
|
saveSession(chatSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveSession(ChatSession chatSession) {
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Saving chat session");
|
||||||
|
try {
|
||||||
|
sqlSession.getMapper(ChatSessionMapper.class).storeSession(chatSession);
|
||||||
|
log.debug("Saved chat session");
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to save chat session", e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private synchronized void setUniqueTimestamp(ChatMessage chatMessage) {
|
private synchronized void setUniqueTimestamp(ChatMessage chatMessage) {
|
||||||
long timestamp = chatMessage.getTimestamp().toEpochMilli();
|
long timestamp = chatMessage.getTimestamp().toEpochMilli();
|
||||||
if (timestamp == lastMillis) {
|
if (timestamp == lastMillis) {
|
||||||
|
|
@ -88,7 +127,7 @@ public class ChatService {
|
||||||
jsonCache.put(message, ChatMessageMapper.toJson(message));
|
jsonCache.put(message, ChatMessageMapper.toJson(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
eventPublisher.sendToUsers("chat", (eventUser) ->
|
sendToUsers("chat", (eventUser) ->
|
||||||
chatMessageList.stream()
|
chatMessageList.stream()
|
||||||
.filter(chatMessage -> shouldReceive(eventUser, chatMessage))
|
.filter(chatMessage -> shouldReceive(eventUser, chatMessage))
|
||||||
.map(jsonCache::get)
|
.map(jsonCache::get)
|
||||||
|
|
@ -96,6 +135,19 @@ public class ChatService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcasts to every currently subscribed key. The message content for
|
||||||
|
* each key is resolved via messageForUser, which now works off the
|
||||||
|
* unique key itself rather than an EventUser instance.
|
||||||
|
*/
|
||||||
|
public void sendToUsers(String eventName, MessageForUser messageForUser) {
|
||||||
|
eventUserMap.forEach((key, eventUser) -> {
|
||||||
|
String json = messageForUser.get(eventUser);
|
||||||
|
eventPublisher.sendToUser(key, eventName, json);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private boolean shouldReceive(EventUser eventUser, ChatMessage chatMessage) {
|
private boolean shouldReceive(EventUser eventUser, ChatMessage chatMessage) {
|
||||||
if (eventUser.hasPermission(PermissionClaimDto.HEAD_MOD)) {
|
if (eventUser.hasPermission(PermissionClaimDto.HEAD_MOD)) {
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,5 @@
|
||||||
package com.alttd.altitudeweb.services.chat.event_publisher;
|
package com.alttd.altitudeweb.services.chat.event_publisher;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
|
||||||
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
|
||||||
import com.alttd.altitudeweb.database.web_db.chat_session.ChatSession;
|
|
||||||
import com.alttd.altitudeweb.database.web_db.chat_session.ChatSessionMapper;
|
|
||||||
import com.alttd.altitudeweb.setup.Connection;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
|
@ -17,27 +12,44 @@ import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.CopyOnWriteArrayList;
|
import java.util.concurrent.CopyOnWriteArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic SSE event publisher, keyed by an arbitrary "unique" string
|
||||||
|
* The same key may be used by several concurrent connections
|
||||||
|
* (e.g., a user/server with multiple open tabs or connections).
|
||||||
|
* Each subscribe() call adds its own emitter under that key
|
||||||
|
* without evicting the others.
|
||||||
|
*/
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Service
|
@Service
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class EventPublisher {
|
public class EventPublisher {
|
||||||
|
|
||||||
private final Map<UUID, EventUser> eventUserMap = new ConcurrentHashMap<>();
|
private final Map<String, List<SseEmitter>> emitterMap = new ConcurrentHashMap<>();
|
||||||
private final Map<UUID, List<SseEmitter>> emitterMap = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
public SseEmitter subscribe(EventUser eventUser, String initialData) {
|
/**
|
||||||
|
* Invoked once a given emitter for uniqueKey is cleaned up (completed,
|
||||||
|
* timed out, or errored). Callers use this to do whatever bookkeeping
|
||||||
|
* they need (e.g., persisting a Session) using sessionStart/sessionEnd.
|
||||||
|
*/
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface SessionCleanupCallback {
|
||||||
|
void onCleanup(String uniqueKey, Instant sessionStart, Instant sessionEnd);
|
||||||
|
}
|
||||||
|
public SseEmitter subscribe(String uniqueKey, SessionCleanupCallback cleanupCallback) {
|
||||||
|
return subscribe(uniqueKey, null, cleanupCallback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SseEmitter subscribe(String uniqueKey, String initialData, SessionCleanupCallback cleanupCallback) {
|
||||||
SseEmitter emitter = new SseEmitter(0L); // no built-in timeout; heartbeat governs liveness
|
SseEmitter emitter = new SseEmitter(0L); // no built-in timeout; heartbeat governs liveness
|
||||||
|
|
||||||
emitterMap.computeIfAbsent(eventUser.uuid(), k -> new CopyOnWriteArrayList<>()).add(emitter);
|
emitterMap.computeIfAbsent(uniqueKey, k -> new CopyOnWriteArrayList<>()).add(emitter);
|
||||||
eventUserMap.put(eventUser.uuid(), eventUser);
|
|
||||||
Instant sessionStart = Instant.now();
|
Instant sessionStart = Instant.now();
|
||||||
log.info("User {} subscribed, active emitters: {}", eventUser.uuid(), countActive());
|
log.info("Key {} subscribed, active emitters: {}", uniqueKey, countActive());
|
||||||
|
|
||||||
Runnable cleanup = () -> handleCleanup(eventUser, emitter, sessionStart);
|
Runnable cleanup = () -> handleCleanup(uniqueKey, emitter, sessionStart, cleanupCallback);
|
||||||
|
|
||||||
// covers: clean disconnect (tab closed normally), server-side timeout, write failure
|
// covers: clean disconnect (tab closed normally), server-side timeout, write failure
|
||||||
emitter.onCompletion(cleanup);
|
emitter.onCompletion(cleanup);
|
||||||
|
|
@ -45,7 +57,11 @@ public class EventPublisher {
|
||||||
emitter.onError(e -> cleanup.run());
|
emitter.onError(e -> cleanup.run());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (initialData != null) {
|
||||||
emitter.send(SseEmitter.event().name("connected").data(initialData));
|
emitter.send(SseEmitter.event().name("connected").data(initialData));
|
||||||
|
} else {
|
||||||
|
emitter.send(SseEmitter.event().name("connected"));
|
||||||
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
cleanup.run();
|
cleanup.run();
|
||||||
}
|
}
|
||||||
|
|
@ -71,8 +87,8 @@ public class EventPublisher {
|
||||||
emitterMap.entrySet().removeIf(e -> e.getValue().isEmpty());
|
emitterMap.entrySet().removeIf(e -> e.getValue().isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendToUser(UUID uuid, String eventName, String json) {
|
public void sendToUser(String uniqueKey, String eventName, String json) {
|
||||||
List<SseEmitter> userEmitters = emitterMap.get(uuid);
|
List<SseEmitter> userEmitters = emitterMap.get(uniqueKey);
|
||||||
if (userEmitters == null) {
|
if (userEmitters == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -92,45 +108,23 @@ public class EventPublisher {
|
||||||
return emitterMap.values().stream().mapToLong(List::size).sum();
|
return emitterMap.values().stream().mapToLong(List::size).sum();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendToUsers(String eventName, MessageForUser messagesForUser) {
|
private void handleCleanup(String uniqueKey, SseEmitter emitter, Instant sessionStart,
|
||||||
eventUserMap.values().forEach(emitter -> {
|
SessionCleanupCallback cleanupCallback) {
|
||||||
String json = messagesForUser.get(emitter);
|
List<SseEmitter> userEmitters = emitterMap.get(uniqueKey);
|
||||||
sendToUser(emitter.uuid(), eventName, json);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void handleCleanup(EventUser eventUser, SseEmitter emitter, Instant sessionStart) {
|
|
||||||
List<SseEmitter> userEmitters = emitterMap.get(eventUser.uuid());
|
|
||||||
if (userEmitters != null) {
|
if (userEmitters != null) {
|
||||||
userEmitters.remove(emitter);
|
userEmitters.remove(emitter);
|
||||||
if (userEmitters.isEmpty()) {
|
if (userEmitters.isEmpty()) {
|
||||||
emitterMap.remove(eventUser.uuid());
|
emitterMap.remove(uniqueKey);
|
||||||
eventUserMap.remove(eventUser.uuid());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Instant sessionEnd = Instant.now();
|
Instant sessionEnd = Instant.now();
|
||||||
Duration between = Duration.between(sessionStart, sessionEnd);
|
Duration between = Duration.between(sessionStart, sessionEnd);
|
||||||
log.info("Cleaned up emitter for {}, active emitters: {}, session duration: {}",
|
log.info("Cleaned up emitter for {}, active emitters: {}, session duration: {}",
|
||||||
eventUser.uuid(), countActive(), between
|
uniqueKey, countActive(), between
|
||||||
);
|
);
|
||||||
ChatSession chatSession = ChatSession.builder()
|
|
||||||
.uuid(eventUser.uuid())
|
|
||||||
.session_start(sessionStart)
|
|
||||||
.session_end(sessionEnd)
|
|
||||||
.build();
|
|
||||||
saveSession(chatSession);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveSession(ChatSession chatSession) {
|
if (cleanupCallback != null) {
|
||||||
Connection.getConnection(Databases.DEFAULT)
|
cleanupCallback.onCleanup(uniqueKey, sessionStart, sessionEnd);
|
||||||
.runQuery(sqlSession -> {
|
|
||||||
log.debug("Saving chat session");
|
|
||||||
try {
|
|
||||||
sqlSession.getMapper(ChatSessionMapper.class).storeSession(chatSession);
|
|
||||||
log.debug("Saved chat session");
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error("Failed to save chat session", e);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
package com.alttd.altitudeweb.services.chat.to_server;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.services.chat.event_publisher.EventPublisher;
|
||||||
|
import com.alttd.altitudeweb.services.chat.event_publisher.EventUser;
|
||||||
|
import com.alttd.altitudeweb.services.chat.to_server.data.ChatFromWeb;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ServerMessageService {
|
||||||
|
|
||||||
|
private final Set<String> servers = new HashSet<>();
|
||||||
|
private final EventPublisher eventPublisher;
|
||||||
|
|
||||||
|
public SseEmitter subscribe(String server) {
|
||||||
|
servers.add(server);
|
||||||
|
return eventPublisher.subscribe(server, this::handleSessionEnd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleSessionEnd(String key, Instant sessionStart, Instant sessionEnd) {
|
||||||
|
log.info("Server session ended for {}. Active from {} for {}", key, sessionStart, Duration.between(sessionStart, sessionEnd));
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean sendMessage(String server, String channel, String json) {
|
||||||
|
if (!servers.contains(server)) {
|
||||||
|
log.warn("Server {} is not connected, not sending message", server);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
eventPublisher.sendToUser(server, channel, json);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 10_000)
|
||||||
|
public void sendTestMessage() {
|
||||||
|
ChatFromWeb chatFromWeb = ChatFromWeb.builder().sender(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"))
|
||||||
|
.message("This is a test <red>a cool test</red>").build();
|
||||||
|
if (!sendMessage("alpha", "web_chat", toJson(chatFromWeb))) {
|
||||||
|
log.warn("Failed to send test message");
|
||||||
|
}
|
||||||
|
log.info("Sent test message");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||||
|
|
||||||
|
public static String toJson(Object object) {
|
||||||
|
try {
|
||||||
|
return OBJECT_MAPPER.writeValueAsString(object);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalStateException("Failed to serialize object to JSON", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package com.alttd.altitudeweb.services.chat.to_server.data;
|
||||||
|
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Builder
|
||||||
|
@Getter
|
||||||
|
public class ChatFromWeb {
|
||||||
|
|
||||||
|
private final UUID sender;
|
||||||
|
private final String message;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -24,3 +24,4 @@ management.endpoints.web.exposure.include=mappings
|
||||||
server.forward-headers-strategy=native
|
server.forward-headers-strategy=native
|
||||||
server.tomcat.remoteip.remote-ip-header=CF-Connecting-IP
|
server.tomcat.remoteip.remote-ip-header=CF-Connecting-IP
|
||||||
server.tomcat.remoteip.trusted-proxies=10\\.0\\.0\\.103
|
server.tomcat.remoteip.trusted-proxies=10\\.0\\.0\\.103
|
||||||
|
altitudeweb.token=${TOKEN:invalid-token}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@ package com.alttd.altitudeweb.services.chat;
|
||||||
import com.alttd.altitudeweb.controllers.chat.ChatMessage;
|
import com.alttd.altitudeweb.controllers.chat.ChatMessage;
|
||||||
import com.alttd.altitudeweb.controllers.chat.ChatMessageType;
|
import com.alttd.altitudeweb.controllers.chat.ChatMessageType;
|
||||||
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
||||||
import com.alttd.altitudeweb.services.chat.event_publisher.EventPublisher;
|
|
||||||
import com.alttd.altitudeweb.services.chat.event_publisher.EventUser;
|
import com.alttd.altitudeweb.services.chat.event_publisher.EventUser;
|
||||||
import com.alttd.altitudeweb.services.chat.event_publisher.MessageForUser;
|
import com.alttd.altitudeweb.services.chat.event_publisher.MessageForUser;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
|
@ -23,12 +22,10 @@ import static org.mockito.Mockito.*;
|
||||||
class ChatServiceTest {
|
class ChatServiceTest {
|
||||||
|
|
||||||
private ChatService chatService;
|
private ChatService chatService;
|
||||||
private EventPublisher eventPublisher;
|
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() {
|
void setUp() {
|
||||||
eventPublisher = mock(EventPublisher.class);
|
chatService = spy(new ChatService());
|
||||||
chatService = new ChatService(eventPublisher);
|
|
||||||
ReflectionTestUtils.setField(chatService, "allowedServers", new String[]{"server1"});
|
ReflectionTestUtils.setField(chatService, "allowedServers", new String[]{"server1"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +43,7 @@ class ChatServiceTest {
|
||||||
chatService.addChatMessage(List.of(message));
|
chatService.addChatMessage(List.of(message));
|
||||||
|
|
||||||
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
|
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
|
||||||
verify(eventPublisher).sendToUsers(anyString(), captor.capture());
|
verify(chatService).sendToUsers(anyString(), captor.capture());
|
||||||
|
|
||||||
MessageForUser messageForUser = captor.getValue();
|
MessageForUser messageForUser = captor.getValue();
|
||||||
EventUser user = new EventUser(UUID.randomUUID(), List.of(), null);
|
EventUser user = new EventUser(UUID.randomUUID(), List.of(), null);
|
||||||
|
|
@ -69,7 +66,7 @@ class ChatServiceTest {
|
||||||
chatService.addChatMessage(List.of(message));
|
chatService.addChatMessage(List.of(message));
|
||||||
|
|
||||||
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
|
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
|
||||||
verify(eventPublisher).sendToUsers(anyString(), captor.capture());
|
verify(chatService).sendToUsers(anyString(), captor.capture());
|
||||||
|
|
||||||
MessageForUser messageForUser = captor.getValue();
|
MessageForUser messageForUser = captor.getValue();
|
||||||
|
|
||||||
|
|
@ -97,7 +94,7 @@ class ChatServiceTest {
|
||||||
chatService.addChatMessage(List.of(message));
|
chatService.addChatMessage(List.of(message));
|
||||||
|
|
||||||
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
|
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
|
||||||
verify(eventPublisher).sendToUsers(anyString(), captor.capture());
|
verify(chatService).sendToUsers(anyString(), captor.capture());
|
||||||
|
|
||||||
MessageForUser messageForUser = captor.getValue();
|
MessageForUser messageForUser = captor.getValue();
|
||||||
|
|
||||||
|
|
@ -123,7 +120,7 @@ class ChatServiceTest {
|
||||||
chatService.addChatMessage(List.of(message));
|
chatService.addChatMessage(List.of(message));
|
||||||
|
|
||||||
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
|
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
|
||||||
verify(eventPublisher).sendToUsers(anyString(), captor.capture());
|
verify(chatService).sendToUsers(anyString(), captor.capture());
|
||||||
|
|
||||||
MessageForUser messageForUser = captor.getValue();
|
MessageForUser messageForUser = captor.getValue();
|
||||||
EventUser user = new EventUser(UUID.randomUUID(), List.of(), null);
|
EventUser user = new EventUser(UUID.randomUUID(), List.of(), null);
|
||||||
|
|
@ -144,7 +141,7 @@ class ChatServiceTest {
|
||||||
chatService.addChatMessage(List.of(message));
|
chatService.addChatMessage(List.of(message));
|
||||||
|
|
||||||
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
|
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
|
||||||
verify(eventPublisher).sendToUsers(anyString(), captor.capture());
|
verify(chatService).sendToUsers(anyString(), captor.capture());
|
||||||
|
|
||||||
MessageForUser messageForUser = captor.getValue();
|
MessageForUser messageForUser = captor.getValue();
|
||||||
EventUser regularUser = new EventUser(UUID.randomUUID(), List.of(), null);
|
EventUser regularUser = new EventUser(UUID.randomUUID(), List.of(), null);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user