Compare commits

..

2 Commits

5 changed files with 244 additions and 17 deletions

View File

@ -1,10 +1,13 @@
package com.alttd.altitudeweb.controllers.event; 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.chat.ChatLogMapper;
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;
import com.alttd.altitudeweb.services.chat.event_publisher.EventUser; import com.alttd.altitudeweb.services.chat.event_publisher.EventUser;
import com.alttd.altitudeweb.setup.Connection;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -21,7 +24,9 @@ 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.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@RequiredArgsConstructor @RequiredArgsConstructor
@ -62,7 +67,29 @@ public class EventController {
.map(ChatMessageMapper::toJson) .map(ChatMessageMapper::toJson)
.collect(Collectors.joining(",", "[", "]")); .collect(Collectors.joining(",", "[", "]"));
return eventPublisher.subscribe(new EventUser(subject, authorities), json); Integer partyId = getPartyId(subject);
return eventPublisher.subscribe(new EventUser(subject, authorities, partyId), json);
}
private Integer getPartyId(UUID subject) {
CompletableFuture<Integer> partyIdFuture = new CompletableFuture<>();
Connection.getConnection(Databases.CHAT)
.runQuery(sqlSession -> {
log.debug("Loading party id for uuid {}", subject);
try {
Integer partyId = sqlSession.getMapper(ChatLogMapper.class)
.getPartyId(subject.toString())
.orElse(null);
partyIdFuture.complete(partyId);
} catch (Exception e) {
log.error("Failed to load party id", e);
partyIdFuture.complete(null);
}
});
return partyIdFuture.join();
} }
private static boolean doesNotHavePermission(List<String> authorities) { private static boolean doesNotHavePermission(List<String> authorities) {

View File

@ -7,6 +7,7 @@ import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.chat.ChatLogMapper; import com.alttd.altitudeweb.database.chat.ChatLogMapper;
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.setup.Connection; import com.alttd.altitudeweb.setup.Connection;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -83,23 +84,58 @@ public class ChatService {
} }
private void sendMessagesToListeners(List<ChatMessage> chatMessageList) { private void sendMessagesToListeners(List<ChatMessage> chatMessageList) {
String jsonMessageListIncludingBlocked = chatMessageList.stream() Map<ChatMessage, String> jsonCache = new HashMap<>();
.map(ChatMessageMapper::toJson) for (ChatMessage message : chatMessageList) {
.collect(Collectors.joining(",", "[", "]")); jsonCache.put(message, ChatMessageMapper.toJson(message));
String jsonMessageList = chatMessageList.stream() }
.filter(ChatMessage::isNotBlocked)
.filter(chatMessage -> Arrays.asList(allowedServers).contains(chatMessage.getServer()))
//TODO [Stijn] [2026-07-19]: Handle different channel types
.filter(chatMessage -> chatMessage.getType() == ChatMessageType.PUBLIC)
.map(ChatMessageMapper::toJson)
.collect(Collectors.joining(",", "[", "]"));
eventPublisher.sendToUsers("chat", (eventUser) -> { eventPublisher.sendToUsers("chat", (eventUser) ->
if (eventUser.hasPermission(PermissionClaimDto.HEAD_MOD)) { chatMessageList.stream()
return jsonMessageListIncludingBlocked; .filter(chatMessage -> shouldReceive(eventUser, chatMessage))
.map(jsonCache::get)
.collect(Collectors.joining(",", "[", "]"))
);
}
private boolean shouldReceive(EventUser eventUser, ChatMessage chatMessage) {
if (eventUser.hasPermission(PermissionClaimDto.HEAD_MOD)) {
return true;
}
if (!Arrays.asList(allowedServers).contains(chatMessage.getServer())) {
return false;
}
if (!chatMessage.isNotBlocked()) {
return false;
}
return switch (chatMessage.getType()) {
case PUBLIC, GLOBAL -> true;
case PARTY -> {
Integer partyId = eventUser.partyId();
if (partyId == null) {
yield false;
}
yield chatMessage.getChannel().equals(String.valueOf(partyId));
} }
return jsonMessageList; case MSG -> {
}); if (eventUser.uuid().equals(chatMessage.getUuid())) {
yield true;
}
if (chatMessage.getReceiver() != null) {
try {
UUID receiverUuid = UUID.fromString(chatMessage.getReceiver());
yield eventUser.uuid().equals(receiverUuid);
} catch (IllegalArgumentException e) {
yield false;
}
}
yield false;
}
case GAC -> eventUser.hasPermission(PermissionClaimDto.MOD);
case CUSTOM -> false;
};
} }
public List<ChatMessage> getMessagesSince(Instant instant) { public List<ChatMessage> getMessagesSince(Instant instant) {

View File

@ -6,7 +6,7 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
public record EventUser(UUID uuid, List<String> authorities) { public record EventUser(UUID uuid, List<String> authorities, Integer partyId) {
public boolean hasPermission(PermissionClaimDto permission) { public boolean hasPermission(PermissionClaimDto permission) {
return authorities.contains(permission.getValue()); return authorities.contains(permission.getValue());

View File

@ -0,0 +1,157 @@
package com.alttd.altitudeweb.services.chat;
import com.alttd.altitudeweb.controllers.chat.ChatMessage;
import com.alttd.altitudeweb.controllers.chat.ChatMessageType;
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.MessageForUser;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.util.ReflectionTestUtils;
import java.time.Instant;
import java.util.List;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
class ChatServiceTest {
private ChatService chatService;
private EventPublisher eventPublisher;
@BeforeEach
void setUp() {
eventPublisher = mock(EventPublisher.class);
chatService = new ChatService(eventPublisher);
ReflectionTestUtils.setField(chatService, "allowedServers", new String[]{"server1"});
}
@Test
void testPublicMessageSentToEveryone() {
ChatMessage message = ChatMessage.builder()
.uuid(UUID.randomUUID())
.type(ChatMessageType.PUBLIC)
.server("server1")
.messageJson("hello")
.notBlocked(true)
.timestamp(Instant.now())
.build();
chatService.addChatMessage(List.of(message));
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
verify(eventPublisher).sendToUsers(anyString(), captor.capture());
MessageForUser messageForUser = captor.getValue();
EventUser user = new EventUser(UUID.randomUUID(), List.of());
String result = messageForUser.get(user);
assertTrue(result.contains("hello"));
}
@Test
void testGacMessageSentOnlyToStaff() {
ChatMessage message = ChatMessage.builder()
.uuid(UUID.randomUUID())
.type(ChatMessageType.GAC)
.server("server1")
.messageJson("staff chat")
.notBlocked(true)
.timestamp(Instant.now())
.build();
chatService.addChatMessage(List.of(message));
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
verify(eventPublisher).sendToUsers(anyString(), captor.capture());
MessageForUser messageForUser = captor.getValue();
EventUser regularUser = new EventUser(UUID.randomUUID(), List.of());
assertFalse(messageForUser.get(regularUser).contains("staff chat"), "Regular user should not see GAC message");
EventUser modUser = new EventUser(UUID.randomUUID(), List.of(PermissionClaimDto.MOD.getValue()));
assertTrue(messageForUser.get(modUser).contains("staff chat"), "MOD should see GAC message");
}
@Test
void testMsgMessageSentToSenderAndReceiver() {
UUID senderUuid = UUID.randomUUID();
UUID receiverUuid = UUID.randomUUID();
ChatMessage message = ChatMessage.builder()
.uuid(senderUuid)
.type(ChatMessageType.MSG)
.server("server1")
.receiver(receiverUuid.toString())
.messageJson("private message")
.notBlocked(true)
.timestamp(Instant.now())
.build();
chatService.addChatMessage(List.of(message));
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
verify(eventPublisher).sendToUsers(anyString(), captor.capture());
MessageForUser messageForUser = captor.getValue();
EventUser sender = new EventUser(senderUuid, List.of());
EventUser receiver = new EventUser(receiverUuid, List.of());
EventUser other = new EventUser(UUID.randomUUID(), List.of());
assertTrue(messageForUser.get(sender).contains("private message"), "Sender should see their own message");
assertTrue(messageForUser.get(receiver).contains("private message"), "Receiver should see the message");
assertFalse(messageForUser.get(other).contains("private message"), "Others should not see private message");
}
@Test
void testGlobalMessageSentToEveryone() {
ChatMessage message = ChatMessage.builder()
.uuid(UUID.randomUUID())
.type(ChatMessageType.GLOBAL)
.server("server1")
.messageJson("global hello")
.notBlocked(true)
.timestamp(Instant.now())
.build();
chatService.addChatMessage(List.of(message));
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
verify(eventPublisher).sendToUsers(anyString(), captor.capture());
MessageForUser messageForUser = captor.getValue();
EventUser user = new EventUser(UUID.randomUUID(), List.of());
assertTrue(messageForUser.get(user).contains("global hello"));
}
@Test
void testBlockedMessageOnlySentToHeadMod() {
ChatMessage message = ChatMessage.builder()
.uuid(UUID.randomUUID())
.type(ChatMessageType.PUBLIC)
.server("server1")
.messageJson("blocked message")
.notBlocked(false)
.timestamp(Instant.now())
.build();
chatService.addChatMessage(List.of(message));
ArgumentCaptor<MessageForUser> captor = ArgumentCaptor.forClass(MessageForUser.class);
verify(eventPublisher).sendToUsers(anyString(), captor.capture());
MessageForUser messageForUser = captor.getValue();
EventUser regularUser = new EventUser(UUID.randomUUID(), List.of());
EventUser headModUser = new EventUser(UUID.randomUUID(), List.of(PermissionClaimDto.HEAD_MOD.getValue()));
assertFalse(messageForUser.get(regularUser).contains("blocked message"));
assertTrue(messageForUser.get(headModUser).contains("blocked message"));
}
}

View File

@ -4,6 +4,7 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Select;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface ChatLogMapper { public interface ChatLogMapper {
@ -15,4 +16,10 @@ public interface ChatLogMapper {
""") """)
List<ChatLogDao> getChatLogs(@Param("since") long since); List<ChatLogDao> getChatLogs(@Param("since") long since);
@Select("""
SELECT party_id FROM chat_users
WHERE uuid = #{uuid}
""")
Optional<Integer> getPartyId(@Param("uuid") String uuid);
} }