Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public class NotificationService {
""" + ChatConstants.HELP_SUGGESTION;

private static final int GREETING_NOTIFICATION_ORDER = Ordered.HIGHEST_PRECEDENCE;
private static final int USER_INTERACTION_NOTIFICATION_ORDER = GREETING_NOTIFICATION_ORDER + 1000;
static final int USER_INTERACTION_NOTIFICATION_ORDER = GREETING_NOTIFICATION_ORDER + 1000;
private static final int DAILY_RANKING_NOTIFICATION_ORDER = 0;

private final CustomTelegramClient customTelegramClient;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package dev.rubasace.linkedin.games.ldrbot.chat;

import dev.rubasace.linkedin.games.ldrbot.configuration.ExecutorsConfiguration;
import dev.rubasace.linkedin.games.ldrbot.session.GameSession;
import dev.rubasace.linkedin.games.ldrbot.session.GameSessionRegistrationEvent;
import dev.rubasace.linkedin.games.ldrbot.session.GameSessionRepository;
import dev.rubasace.linkedin.games.ldrbot.util.FormatUtils;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

import java.time.Duration;
import java.util.List;

@Service
public class RecordNotificationService {

private static final String NEW_RECORD_MESSAGE = "🏆 New record! %s completed %s in %s, beating the previous record of %s!";
private static final String FIRST_RECORD_MESSAGE = "🏆 New record! %s completed %s in %s!";

private final GameSessionRepository gameSessionRepository;
private final CustomTelegramClient customTelegramClient;

RecordNotificationService(final GameSessionRepository gameSessionRepository, final CustomTelegramClient customTelegramClient) {
this.gameSessionRepository = gameSessionRepository;
this.customTelegramClient = customTelegramClient;
}

@Order(NotificationService.USER_INTERACTION_NOTIFICATION_ORDER)
@Async(ExecutorsConfiguration.NOTIFICATION_LISTENER_EXECUTOR_NAME)
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
void handleRecordNotification(final GameSessionRegistrationEvent event) {
List<GameSession> top2Sessions = gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc(
event.getChatId(),
event.getGameInfo().type()
);

if (top2Sessions.isEmpty()) {
return;
}

GameSession bestSession = top2Sessions.get(0);
Duration bestDuration = bestSession.getDuration();

if (!bestDuration.equals(event.getDuration())) {
return;
}

String userMention = FormatUtils.formatUserMention(event.getUserInfo());
String gameName = event.getGameInfo().name();
String formattedDuration = FormatUtils.formatDuration(bestDuration);

String message;
if (top2Sessions.size() == 1) {
message = FIRST_RECORD_MESSAGE.formatted(userMention, gameName, formattedDuration);
} else {
Duration previousRecord = top2Sessions.get(1).getDuration();
String formattedPreviousRecord = FormatUtils.formatDuration(previousRecord);
message = NEW_RECORD_MESSAGE.formatted(userMention, gameName, formattedDuration, formattedPreviousRecord);
}

customTelegramClient.sendMessage(message, event.getChatInfo().chatId());
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package dev.rubasace.linkedin.games.ldrbot.session;

public record GameInfo(String name, String icon) {
public record GameInfo(GameType type, String name, String icon) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
Expand All @@ -25,4 +26,6 @@ public interface GameSessionRepository extends JpaRepository<GameSession, UUID>

@Transactional
void deleteByUserIdAndGroupChatIdAndGameDay(Long UserId, Long chatId, LocalDate gameDay);

List<GameSession> findTop2ByGroupChatIdAndGameOrderByDurationAsc(Long chatId, GameType game);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
public class GameTypeAdapter {

public GameInfo adapt(final GameType gameType) {
return new GameInfo(StringUtils.capitalize(gameType.name().toLowerCase()), gameIcon(gameType));
return new GameInfo(gameType, StringUtils.capitalize(gameType.name().toLowerCase()), gameIcon(gameType));
}

private static String gameIcon(final GameType gameType) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
package dev.rubasace.linkedin.games.ldrbot.chat;

import dev.rubasace.linkedin.games.ldrbot.group.ChatInfo;
import dev.rubasace.linkedin.games.ldrbot.session.GameInfo;
import dev.rubasace.linkedin.games.ldrbot.session.GameSession;
import dev.rubasace.linkedin.games.ldrbot.session.GameSessionRegistrationEvent;
import dev.rubasace.linkedin.games.ldrbot.session.GameSessionRepository;
import dev.rubasace.linkedin.games.ldrbot.session.GameType;
import dev.rubasace.linkedin.games.ldrbot.user.UserInfo;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.Duration;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class RecordNotificationServiceTest {

@Mock
private GameSessionRepository gameSessionRepository;

@Mock
private CustomTelegramClient customTelegramClient;

@InjectMocks
private RecordNotificationService recordNotificationService;

@Test
void shouldNotNotifyWhenNotARecord() {
GameSessionRegistrationEvent event = createEvent(Duration.ofMinutes(2));
GameSession bestSession = createGameSession(Duration.ofMinutes(1));

when(gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc(anyLong(), eq(GameType.QUEENS)))
.thenReturn(List.of(bestSession));

recordNotificationService.handleRecordNotification(event);

verify(customTelegramClient, never()).sendMessage(org.mockito.ArgumentMatchers.anyString(), anyLong());
}

@Test
void shouldNotifyWhenFirstRecord() {
Duration recordDuration = Duration.ofMinutes(1);
GameSessionRegistrationEvent event = createEvent(recordDuration);
GameSession bestSession = createGameSession(recordDuration);

when(gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc(anyLong(), eq(GameType.QUEENS)))
.thenReturn(List.of(bestSession));

recordNotificationService.handleRecordNotification(event);

ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
verify(customTelegramClient).sendMessage(messageCaptor.capture(), eq(123L));

String message = messageCaptor.getValue();
assertTrue(message.contains("🏆"));
assertTrue(message.contains("New record"));
assertTrue(message.contains("@testuser"));
assertTrue(message.contains("Queens"));
assertTrue(message.contains("01:00"));
assertFalse(message.contains("beating"));
}

@Test
void shouldNotifyWhenBrokenRecord() {
Duration recordDuration = Duration.ofMinutes(1);
Duration previousRecord = Duration.ofMinutes(2);
GameSessionRegistrationEvent event = createEvent(recordDuration);
GameSession bestSession = createGameSession(recordDuration);
GameSession secondBestSession = createGameSession(previousRecord);

when(gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc(anyLong(), eq(GameType.QUEENS)))
.thenReturn(List.of(bestSession, secondBestSession));

recordNotificationService.handleRecordNotification(event);

ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
verify(customTelegramClient).sendMessage(messageCaptor.capture(), eq(123L));

String message = messageCaptor.getValue();
assertTrue(message.contains("🏆"));
assertTrue(message.contains("New record"));
assertTrue(message.contains("@testuser"));
assertTrue(message.contains("Queens"));
assertTrue(message.contains("01:00"));
assertTrue(message.contains("beating"));
assertTrue(message.contains("02:00"));
}

@Test
void shouldHandleUserWithoutUsername() {
Duration recordDuration = Duration.ofMinutes(1);
UserInfo userWithoutUsername = new UserInfo(456L, null, "John", "Doe");
GameSessionRegistrationEvent event = createEventWithUser(recordDuration, userWithoutUsername);
GameSession bestSession = createGameSession(recordDuration);

when(gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc(anyLong(), eq(GameType.QUEENS)))
.thenReturn(List.of(bestSession));

recordNotificationService.handleRecordNotification(event);

ArgumentCaptor<String> messageCaptor = ArgumentCaptor.forClass(String.class);
verify(customTelegramClient).sendMessage(messageCaptor.capture(), eq(123L));

String message = messageCaptor.getValue();
assertTrue(message.contains("<a href=\"tg://user?id=456\">"));
assertTrue(message.contains("John Doe"));
}

@Test
void shouldNotNotifyWhenNoSessions() {
GameSessionRegistrationEvent event = createEvent(Duration.ofMinutes(1));

when(gameSessionRepository.findTop2ByGroupChatIdAndGameOrderByDurationAsc(anyLong(), eq(GameType.QUEENS)))
.thenReturn(Collections.emptyList());

recordNotificationService.handleRecordNotification(event);

verify(customTelegramClient, never()).sendMessage(org.mockito.ArgumentMatchers.anyString(), anyLong());
}

private GameSessionRegistrationEvent createEvent(final Duration duration) {
UserInfo userInfo = new UserInfo(456L, "testuser", "Test", "User");
return createEventWithUser(duration, userInfo);
}

private GameSessionRegistrationEvent createEventWithUser(final Duration duration, final UserInfo userInfo) {
ChatInfo chatInfo = new ChatInfo(123L, "Test Group", true);
GameInfo gameInfo = new GameInfo(GameType.QUEENS, "Queens", "👑");
return new GameSessionRegistrationEvent(
this,
chatInfo,
userInfo,
gameInfo,
duration,
LocalDate.now(),
123L
);
}

private GameSession createGameSession(final Duration duration) {
GameSession session = new GameSession();
session.setDuration(duration);
return session;
}
}