3 Commits
18 changed files with 491 additions and 25 deletions
@@ -72,6 +72,8 @@ public class SecurityConfig {
.requestMatchers("/api/history/admin/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/api/history/admin/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.requestMatchers("/api/login/userLogin/**").permitAll() .requestMatchers("/api/login/userLogin/**").permitAll()
.requestMatchers("/api/site/chat/**").authenticated() .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() .requestMatchers("/api/chat/server/send/**").permitAll()
.anyRequest().permitAll() .anyRequest().permitAll()
) )
@@ -2,9 +2,19 @@ package com.alttd.altitudeweb.controllers.chat;
import com.alttd.altitudeweb.api.ServerMessageApi; import com.alttd.altitudeweb.api.ServerMessageApi;
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid; 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.model.ServerMessageRequestDto;
import com.alttd.altitudeweb.services.chat.to_server.ServerMessageService; 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.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.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -34,21 +44,47 @@ public class ServerMessageController implements ServerMessageApi {
} }
@Override @Override
public ResponseEntity<Void> sendServerMessage(String server, ServerMessageRequestDto serverMessageRequestDto) { public ResponseEntity<Void> punishPlayer(String server, PunishRequestDto punishRequestDto) throws Exception {
UUID authenticatedUserUuid = authenticatedUuid.getAuthenticatedUserUuid(); validateUser(punishRequestDto.getExecutor());
return send(server, "web_punish", PunishFromWebMapper.fromDto(punishRequestDto));
}
if (!authenticatedUserUuid.equals(serverMessageRequestDto.getUuid())) { @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());
//TODO [Stijn] [2026-08-02]: Validate the user can send a message in this server (or do that in chat)
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"); throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot send message as another user");
} }
}
ChatFromWeb chatFromWeb = ChatFromWeb.builder() private ResponseEntity<Void> send(String server, String channel, Object data) {
.sender(serverMessageRequestDto.getUuid()) String json = toJson(data);
.message(serverMessageRequestDto.getMessage()) if (serverMessageService.sendMessage(server, channel, json)) {
.build();
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 new ResponseEntity<>(HttpStatus.ACCEPTED); return new ResponseEntity<>(HttpStatus.ACCEPTED);
} else { } else {
return new ResponseEntity<>(HttpStatus.NOT_FOUND); 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.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 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 com.alttd.altitudeweb.setup.Connection;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; 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.Duration;
import java.time.Instant; import java.time.Instant;
import java.util.*; import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Slf4j @Slf4j
@@ -35,8 +38,9 @@ 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 EventPublisher eventPublisher;
private final ServerMessageService serverMessageService;
private final NavigableMap<Instant, ChatMessage> chatMessages = new TreeMap<>(); 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<>(); private final Map<String, ServerDto> serverStateCache = new HashMap<>();
@Value("${chat.allowed-servers}") @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(); String key = eventUser.uuid().toString();
eventUserMap.put(key, eventUser); boolean newUser = eventUserMap.put(key, eventUser) == null;
SseEmitter emitter = eventPublisher.subscribe(key, json, this::handleSessionEnd); SseEmitter emitter = eventPublisher.subscribe(key, json, this::handleSessionEnd);
if (newUser) {
sendPlayerStateToServers();
}
sendServerStateToUser(key, eventUser); sendServerStateToUser(key, eventUser);
return emitter; 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); EventUser eventUser = eventUserMap.get(key);
if (eventUser == null) { if (eventUser == null) {
log.error("Failed to find event user for key {}", key); log.error("Failed to find event user for key {}", key);
@@ -91,6 +98,17 @@ public class ChatService {
.session_end(sessionEnd) .session_end(sessionEnd)
.build(); .build();
saveSession(chatSession); 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) { private void saveSession(ChatSession chatSession) {
@@ -104,6 +104,11 @@ public class EventPublisher {
userEmitters.removeAll(dead); userEmitters.removeAll(dead);
} }
public boolean hasSubscribers(String uniqueKey) {
List<SseEmitter> emitters = emitterMap.get(uniqueKey);
return emitters != null && !emitters.isEmpty();
}
private long countActive() { private long countActive() {
return emitterMap.values().stream().mapToLong(List::size).sum(); return emitterMap.values().stream().mapToLong(List::size).sum();
} }
@@ -1,8 +1,6 @@
package com.alttd.altitudeweb.services.chat.to_server; 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.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.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@@ -13,27 +11,48 @@ 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;
import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.concurrent.ConcurrentHashMap;
@Slf4j @Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class ServerMessageService { 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 final EventPublisher eventPublisher;
private volatile String latestPlayerState;
public SseEmitter subscribe(String server) { public synchronized SseEmitter subscribe(String server) {
servers.add(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)); 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) { public boolean sendMessage(String server, String channel, String json) {
if (!servers.contains(server)) { if (!servers.contains(server)) {
log.warn("Server {} is not connected, not sending message", server); log.warn("Server {} is not connected, not sending message", server);
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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.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 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
@@ -18,6 +20,7 @@ import java.util.List;
import java.util.UUID; import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertFalse; 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.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
@@ -26,11 +29,14 @@ import static org.mockito.Mockito.*;
class ChatServiceTest { class ChatServiceTest {
private ChatService chatService; private ChatService chatService;
private EventPublisher eventPublisher;
private ServerMessageService serverMessageService;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
EventPublisher eventPublisher = mock(EventPublisher.class); eventPublisher = mock(EventPublisher.class);
chatService = spy(new ChatService(eventPublisher)); serverMessageService = mock(ServerMessageService.class);
chatService = spy(new ChatService(eventPublisher, serverMessageService));
ReflectionTestUtils.setField(chatService, "allowedServers", new String[]{"server1"}); 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("server1"), "HEAD_MOD should see allowed server");
assertTrue(headModResult.contains("server2"), "HEAD_MOD should see non-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());
}
} }
@@ -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\":[]}");
}
}
+8
View File
@@ -107,3 +107,11 @@ paths:
$ref: './schemas/chat_info/chat_info.yml#/UserName' $ref: './schemas/chat_info/chat_info.yml#/UserName'
/api/site/chat/send-message/{server}: /api/site/chat/send-message/{server}:
$ref: './schemas/server-message/server-message.yml#/sendServerMessage' $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 description: Server not found or not connected
'400': '400':
description: Invalid request 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: components:
parameters: parameters:
Server: Server:
@@ -43,3 +119,74 @@ components:
type: string type: string
description: The message to send description: The message to send
maxLength: 256 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