Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a65f98493 | ||
|
|
423d9c9043 | ||
|
|
7f94569575 |
@@ -72,6 +72,8 @@ public class SecurityConfig {
|
||||
.requestMatchers("/api/history/admin/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/login/userLogin/**").permitAll()
|
||||
.requestMatchers("/api/site/chat/**").authenticated()
|
||||
.requestMatchers("/api/site/chat/send-admin-chat/**").hasAnyAuthority(PermissionClaimDto.HEAD_MOD.getValue(), PermissionClaimDto.MOD.getValue())
|
||||
.requestMatchers("/api/site/chat/punish/**").hasAnyAuthority(PermissionClaimDto.HEAD_MOD.getValue(), PermissionClaimDto.MOD.getValue())
|
||||
.requestMatchers("/api/chat/server/send/**").permitAll()
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
|
||||
+47
-11
@@ -2,9 +2,19 @@ package com.alttd.altitudeweb.controllers.chat;
|
||||
|
||||
import com.alttd.altitudeweb.api.ServerMessageApi;
|
||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||
import com.alttd.altitudeweb.model.PartyMessageRequestDto;
|
||||
import com.alttd.altitudeweb.model.PrivateMessageRequestDto;
|
||||
import com.alttd.altitudeweb.model.PunishRequestDto;
|
||||
import com.alttd.altitudeweb.model.ServerMessageRequestDto;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.ServerMessageService;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.data.ChatFromWeb;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.data.PartyChatFromWeb;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.data.PrivateChatFromWeb;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.data.PunishFromWeb;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.mappers.ChatFromWebMapper;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.mappers.PartyChatFromWebMapper;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.mappers.PrivateChatFromWebMapper;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.mappers.PunishFromWebMapper;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -34,21 +44,47 @@ public class ServerMessageController implements ServerMessageApi {
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> sendServerMessage(String server, ServerMessageRequestDto serverMessageRequestDto) {
|
||||
UUID authenticatedUserUuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
|
||||
if (!authenticatedUserUuid.equals(serverMessageRequestDto.getUuid())) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot send message as another user");
|
||||
public ResponseEntity<Void> punishPlayer(String server, PunishRequestDto punishRequestDto) throws Exception {
|
||||
validateUser(punishRequestDto.getExecutor());
|
||||
return send(server, "web_punish", PunishFromWebMapper.fromDto(punishRequestDto));
|
||||
}
|
||||
|
||||
ChatFromWeb chatFromWeb = ChatFromWeb.builder()
|
||||
.sender(serverMessageRequestDto.getUuid())
|
||||
.message(serverMessageRequestDto.getMessage())
|
||||
.build();
|
||||
@Override
|
||||
public ResponseEntity<Void> sendAdminChat(String server, ServerMessageRequestDto serverMessageRequestDto) {
|
||||
validateUser(serverMessageRequestDto.getUuid());
|
||||
return send(server, "web_ac_chat", ChatFromWebMapper.fromDto(serverMessageRequestDto));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> sendPartyMessage(PartyMessageRequestDto partyMessageRequestDto) {
|
||||
validateUser(partyMessageRequestDto.getFrom());
|
||||
return send("proxy", "web_party_chat", PartyChatFromWebMapper.fromDto(partyMessageRequestDto));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> sendPrivateMessage(PrivateMessageRequestDto privateMessageRequestDto) {
|
||||
validateUser(privateMessageRequestDto.getFrom());
|
||||
return send("proxy", "web_private_chat", PrivateChatFromWebMapper.fromDto(privateMessageRequestDto));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> sendServerMessage(String server, ServerMessageRequestDto serverMessageRequestDto) {
|
||||
validateUser(serverMessageRequestDto.getUuid());
|
||||
|
||||
String json = toJson(chatFromWeb);
|
||||
//TODO [Stijn] [2026-08-02]: Validate the user can send a message in this server (or do that in chat)
|
||||
if (serverMessageService.sendMessage(server, "web_chat", json)) {
|
||||
return send(server, "web_chat", ChatFromWebMapper.fromDto(serverMessageRequestDto));
|
||||
}
|
||||
|
||||
private void validateUser(UUID uuid) {
|
||||
UUID authenticatedUserUuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
if (!authenticatedUserUuid.equals(uuid)) {
|
||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot send message as another user");
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<Void> send(String server, String channel, Object data) {
|
||||
String json = toJson(data);
|
||||
if (serverMessageService.sendMessage(server, channel, json)) {
|
||||
return new ResponseEntity<>(HttpStatus.ACCEPTED);
|
||||
} else {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
|
||||
@@ -13,6 +13,8 @@ import com.alttd.altitudeweb.model.ServerStateDto;
|
||||
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.MessageForUser;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.ServerMessageService;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.mappers.PlayerListStateMapper;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -26,6 +28,7 @@ import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@@ -35,8 +38,9 @@ public class ChatService {
|
||||
|
||||
private static final Duration MAX_AGE = Duration.ofHours(1);
|
||||
private final EventPublisher eventPublisher;
|
||||
private final ServerMessageService serverMessageService;
|
||||
private final NavigableMap<Instant, ChatMessage> chatMessages = new TreeMap<>();
|
||||
private final Map<String, EventUser> eventUserMap = new HashMap<>();
|
||||
private final Map<String, EventUser> eventUserMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, ServerDto> serverStateCache = new HashMap<>();
|
||||
|
||||
@Value("${chat.allowed-servers}")
|
||||
@@ -71,15 +75,18 @@ public class ChatService {
|
||||
});
|
||||
}
|
||||
|
||||
public SseEmitter subscribe(EventUser eventUser, String json) {
|
||||
public synchronized SseEmitter subscribe(EventUser eventUser, String json) {
|
||||
String key = eventUser.uuid().toString();
|
||||
eventUserMap.put(key, eventUser);
|
||||
boolean newUser = eventUserMap.put(key, eventUser) == null;
|
||||
SseEmitter emitter = eventPublisher.subscribe(key, json, this::handleSessionEnd);
|
||||
if (newUser) {
|
||||
sendPlayerStateToServers();
|
||||
}
|
||||
sendServerStateToUser(key, eventUser);
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private void handleSessionEnd(String key, Instant sessionStart, Instant sessionEnd) {
|
||||
private synchronized 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);
|
||||
@@ -91,6 +98,17 @@ public class ChatService {
|
||||
.session_end(sessionEnd)
|
||||
.build();
|
||||
saveSession(chatSession);
|
||||
|
||||
if (!eventPublisher.hasSubscribers(key) && eventUserMap.remove(key, eventUser)) {
|
||||
sendPlayerStateToServers();
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPlayerStateToServers() {
|
||||
ArrayList<UUID> activePlayers = eventUserMap.values().stream()
|
||||
.map(EventUser::uuid)
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
serverMessageService.sendMessageToAll("player_state", PlayerListStateMapper.fromList(activePlayers));
|
||||
}
|
||||
|
||||
private void saveSession(ChatSession chatSession) {
|
||||
|
||||
+5
@@ -104,6 +104,11 @@ public class EventPublisher {
|
||||
userEmitters.removeAll(dead);
|
||||
}
|
||||
|
||||
public boolean hasSubscribers(String uniqueKey) {
|
||||
List<SseEmitter> emitters = emitterMap.get(uniqueKey);
|
||||
return emitters != null && !emitters.isEmpty();
|
||||
}
|
||||
|
||||
private long countActive() {
|
||||
return emitterMap.values().stream().mapToLong(List::size).sum();
|
||||
}
|
||||
|
||||
+27
-8
@@ -1,8 +1,6 @@
|
||||
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;
|
||||
@@ -13,27 +11,48 @@ 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;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ServerMessageService {
|
||||
|
||||
private final Set<String> servers = new HashSet<>();
|
||||
private static final String PLAYER_STATE_CHANNEL = "player_state";
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
private final Set<String> servers = ConcurrentHashMap.newKeySet();
|
||||
private final EventPublisher eventPublisher;
|
||||
private volatile String latestPlayerState;
|
||||
|
||||
public SseEmitter subscribe(String server) {
|
||||
public synchronized SseEmitter subscribe(String server) {
|
||||
servers.add(server);
|
||||
return eventPublisher.subscribe(server, this::handleSessionEnd);
|
||||
SseEmitter emitter = eventPublisher.subscribe(server, this::handleSessionEnd);
|
||||
if (latestPlayerState != null) {
|
||||
eventPublisher.sendToUser(server, PLAYER_STATE_CHANNEL, latestPlayerState);
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private void handleSessionEnd(String key, Instant sessionStart, Instant sessionEnd) {
|
||||
private synchronized void handleSessionEnd(String key, Instant sessionStart, Instant sessionEnd) {
|
||||
log.info("Server session ended for {}. Active from {} for {}", key, sessionStart, Duration.between(sessionStart, sessionEnd));
|
||||
}
|
||||
|
||||
public void sendMessageToAll(String channel, Object data) {
|
||||
String json;
|
||||
try {
|
||||
json = OBJECT_MAPPER.writeValueAsString(data);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new IllegalStateException("Failed to serialize server message", e);
|
||||
}
|
||||
|
||||
if (PLAYER_STATE_CHANNEL.equals(channel)) {
|
||||
latestPlayerState = json;
|
||||
}
|
||||
|
||||
servers.forEach(server -> eventPublisher.sendToUser(server, channel, json));
|
||||
}
|
||||
|
||||
public boolean sendMessage(String server, String channel, String json) {
|
||||
if (!servers.contains(server)) {
|
||||
log.warn("Server {} is not connected, not sending message", server);
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.alttd.altitudeweb.services.chat.to_server.data;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
public class PartyChatFromWeb {
|
||||
|
||||
private final UUID sender;
|
||||
private final String message;
|
||||
private final String partyId;
|
||||
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package com.alttd.altitudeweb.services.chat.to_server.data;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
public class PlayerListState {
|
||||
|
||||
private List<UUID> activePlayers;
|
||||
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.alttd.altitudeweb.services.chat.to_server.data;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
public class PrivateChatFromWeb {
|
||||
|
||||
private final UUID sender;
|
||||
private final String message;
|
||||
private final UUID recipient;
|
||||
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.alttd.altitudeweb.services.chat.to_server.data;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Builder
|
||||
@Getter
|
||||
public class PunishFromWeb {
|
||||
|
||||
private final UUID executor;
|
||||
private final UUID target;
|
||||
private final String type;
|
||||
private final String reason;
|
||||
private final String time;
|
||||
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.alttd.altitudeweb.services.chat.to_server.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.model.ServerMessageRequestDto;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.data.ChatFromWeb;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class ChatFromWebMapper {
|
||||
|
||||
public static ChatFromWeb fromDto(ServerMessageRequestDto dto) {
|
||||
return ChatFromWeb.builder()
|
||||
.sender(dto.getUuid())
|
||||
.message(dto.getMessage())
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.alttd.altitudeweb.services.chat.to_server.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.model.PartyMessageRequestDto;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.data.PartyChatFromWeb;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class PartyChatFromWebMapper {
|
||||
|
||||
public static PartyChatFromWeb fromDto(PartyMessageRequestDto dto) {
|
||||
return PartyChatFromWeb.builder()
|
||||
.sender(dto.getFrom())
|
||||
.message(dto.getMessage())
|
||||
.partyId(dto.getPartyId())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package com.alttd.altitudeweb.services.chat.to_server.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.services.chat.to_server.data.PlayerListState;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.UUID;
|
||||
|
||||
@UtilityClass
|
||||
public class PlayerListStateMapper {
|
||||
|
||||
public static PlayerListState fromList(ArrayList<UUID> activeViewers) {
|
||||
return PlayerListState.builder()
|
||||
.activePlayers(activeViewers)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package com.alttd.altitudeweb.services.chat.to_server.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.model.PrivateMessageRequestDto;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.data.PrivateChatFromWeb;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class PrivateChatFromWebMapper {
|
||||
|
||||
public static PrivateChatFromWeb fromDto(PrivateMessageRequestDto dto) {
|
||||
return PrivateChatFromWeb.builder()
|
||||
.sender(dto.getFrom())
|
||||
.message(dto.getMessage())
|
||||
.recipient(dto.getTo())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package com.alttd.altitudeweb.services.chat.to_server.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.model.PunishRequestDto;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.data.PunishFromWeb;
|
||||
import lombok.experimental.UtilityClass;
|
||||
|
||||
@UtilityClass
|
||||
public class PunishFromWebMapper {
|
||||
|
||||
public static PunishFromWeb fromDto(PunishRequestDto dto) {
|
||||
return PunishFromWeb.builder()
|
||||
.executor(dto.getExecutor())
|
||||
.target(dto.getTarget())
|
||||
.type(dto.getType().getValue())
|
||||
.reason(dto.getReason())
|
||||
.time(dto.getTime())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,6 +8,8 @@ import com.alttd.altitudeweb.model.ServerStateDto;
|
||||
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.MessageForUser;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.ServerMessageService;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.data.PlayerListState;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -18,6 +20,7 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
@@ -26,11 +29,14 @@ import static org.mockito.Mockito.*;
|
||||
class ChatServiceTest {
|
||||
|
||||
private ChatService chatService;
|
||||
private EventPublisher eventPublisher;
|
||||
private ServerMessageService serverMessageService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
EventPublisher eventPublisher = mock(EventPublisher.class);
|
||||
chatService = spy(new ChatService(eventPublisher));
|
||||
eventPublisher = mock(EventPublisher.class);
|
||||
serverMessageService = mock(ServerMessageService.class);
|
||||
chatService = spy(new ChatService(eventPublisher, serverMessageService));
|
||||
ReflectionTestUtils.setField(chatService, "allowedServers", new String[]{"server1"});
|
||||
}
|
||||
|
||||
@@ -179,4 +185,17 @@ class ChatServiceTest {
|
||||
assertTrue(headModResult.contains("server1"), "HEAD_MOD should see allowed server");
|
||||
assertTrue(headModResult.contains("server2"), "HEAD_MOD should see non-allowed server");
|
||||
}
|
||||
|
||||
@Test
|
||||
void subscribingUserBroadcastsActivePlayerStateOnce() {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
EventUser eventUser = new EventUser(uuid, List.of(), null);
|
||||
|
||||
chatService.subscribe(eventUser, "[]");
|
||||
chatService.subscribe(eventUser, "[]");
|
||||
|
||||
ArgumentCaptor<PlayerListState> stateCaptor = ArgumentCaptor.forClass(PlayerListState.class);
|
||||
verify(serverMessageService, times(1)).sendMessageToAll(eq("player_state"), stateCaptor.capture());
|
||||
assertEquals(List.of(uuid), stateCaptor.getValue().getActivePlayers());
|
||||
}
|
||||
}
|
||||
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package com.alttd.altitudeweb.services.chat.to_server;
|
||||
|
||||
import com.alttd.altitudeweb.services.chat.event_publisher.EventPublisher;
|
||||
import com.alttd.altitudeweb.services.chat.to_server.data.PlayerListState;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class ServerMessageServiceTest {
|
||||
|
||||
@Test
|
||||
void broadcastsSerializedPlayerStateToEveryServer() {
|
||||
EventPublisher eventPublisher = mock(EventPublisher.class);
|
||||
when(eventPublisher.subscribe(any(), any(EventPublisher.SessionCleanupCallback.class)))
|
||||
.thenReturn(new SseEmitter());
|
||||
ServerMessageService service = new ServerMessageService(eventPublisher);
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
service.subscribe("server-one");
|
||||
service.subscribe("server-two");
|
||||
service.sendMessageToAll("player_state", PlayerListState.builder()
|
||||
.activePlayers(List.of(uuid))
|
||||
.build());
|
||||
|
||||
String expectedJson = "{\"activePlayers\":[\"" + uuid + "\"]}";
|
||||
verify(eventPublisher).sendToUser("server-one", "player_state", expectedJson);
|
||||
verify(eventPublisher).sendToUser("server-two", "player_state", expectedJson);
|
||||
}
|
||||
|
||||
@Test
|
||||
void newlySubscribedServerReceivesLatestPlayerState() {
|
||||
EventPublisher eventPublisher = mock(EventPublisher.class);
|
||||
when(eventPublisher.subscribe(eq("server-one"), any(EventPublisher.SessionCleanupCallback.class)))
|
||||
.thenReturn(new SseEmitter());
|
||||
ServerMessageService service = new ServerMessageService(eventPublisher);
|
||||
|
||||
service.sendMessageToAll("player_state", PlayerListState.builder()
|
||||
.activePlayers(List.of())
|
||||
.build());
|
||||
service.subscribe("server-one");
|
||||
|
||||
verify(eventPublisher).sendToUser("server-one", "player_state", "{\"activePlayers\":[]}");
|
||||
}
|
||||
}
|
||||
@@ -107,3 +107,11 @@ paths:
|
||||
$ref: './schemas/chat_info/chat_info.yml#/UserName'
|
||||
/api/site/chat/send-message/{server}:
|
||||
$ref: './schemas/server-message/server-message.yml#/sendServerMessage'
|
||||
/api/site/chat/send-admin-chat/{server}:
|
||||
$ref: './schemas/server-message/server-message.yml#/sendAdminChat'
|
||||
/api/site/chat/send-private-message:
|
||||
$ref: './schemas/server-message/server-message.yml#/sendPrivateMessage'
|
||||
/api/site/chat/send-party-message:
|
||||
$ref: './schemas/server-message/server-message.yml#/sendPartyMessage'
|
||||
/api/site/chat/punish/{server}:
|
||||
$ref: './schemas/server-message/server-message.yml#/punishPlayer'
|
||||
|
||||
@@ -19,6 +19,82 @@ sendServerMessage:
|
||||
description: Server not found or not connected
|
||||
'400':
|
||||
description: Invalid request
|
||||
sendAdminChat:
|
||||
post:
|
||||
tags:
|
||||
- server-message
|
||||
summary: Send an admin chat to a specific server
|
||||
operationId: sendAdminChat
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Server'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ServerMessageRequest'
|
||||
responses:
|
||||
'202':
|
||||
description: Message accepted and sent
|
||||
'404':
|
||||
description: Server not found or not connected
|
||||
'400':
|
||||
description: Invalid request
|
||||
sendPrivateMessage:
|
||||
post:
|
||||
tags:
|
||||
- server-message
|
||||
summary: Send a private message
|
||||
operationId: sendPrivateMessage
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PrivateMessageRequest'
|
||||
responses:
|
||||
'202':
|
||||
description: Message accepted and sent
|
||||
'400':
|
||||
description: Invalid request
|
||||
sendPartyMessage:
|
||||
post:
|
||||
tags:
|
||||
- server-message
|
||||
summary: Send a party message
|
||||
operationId: sendPartyMessage
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PartyMessageRequest'
|
||||
responses:
|
||||
'202':
|
||||
description: Message accepted and sent
|
||||
'400':
|
||||
description: Invalid request
|
||||
punishPlayer:
|
||||
post:
|
||||
tags:
|
||||
- server-message
|
||||
summary: Punish a player on a specific server
|
||||
operationId: punishPlayer
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/Server'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/PunishRequest'
|
||||
responses:
|
||||
'202':
|
||||
description: Punishment accepted and applied
|
||||
'404':
|
||||
description: Server not found or not connected
|
||||
'400':
|
||||
description: Invalid request
|
||||
components:
|
||||
parameters:
|
||||
Server:
|
||||
@@ -43,3 +119,74 @@ components:
|
||||
type: string
|
||||
description: The message to send
|
||||
maxLength: 256
|
||||
PrivateMessageRequest:
|
||||
type: object
|
||||
required:
|
||||
- from
|
||||
- message
|
||||
- to
|
||||
properties:
|
||||
from:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The sender UUID
|
||||
message:
|
||||
type: string
|
||||
description: The message to send
|
||||
maxLength: 256
|
||||
to:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The recipient UUID
|
||||
|
||||
PartyMessageRequest:
|
||||
type: object
|
||||
required:
|
||||
- from
|
||||
- message
|
||||
- partyId
|
||||
properties:
|
||||
from:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The sender UUID
|
||||
message:
|
||||
type: string
|
||||
description: The message to send
|
||||
maxLength: 256
|
||||
partyId:
|
||||
type: string
|
||||
format: int32
|
||||
description: The party id
|
||||
|
||||
PunishRequest:
|
||||
type: object
|
||||
required:
|
||||
- executor
|
||||
- target
|
||||
- type
|
||||
- reason
|
||||
properties:
|
||||
executor:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The executor UUID
|
||||
target:
|
||||
type: string
|
||||
format: uuid
|
||||
description: The target UUID
|
||||
type:
|
||||
type: string
|
||||
description: The type of punishment
|
||||
enum:
|
||||
- BAN
|
||||
- MUTE
|
||||
- WARN
|
||||
- FLAG
|
||||
reason:
|
||||
type: string
|
||||
description: The reason for the punishment
|
||||
time:
|
||||
type: string
|
||||
format: duration
|
||||
example: PT30S
|
||||
|
||||
Reference in New Issue
Block a user