diff --git a/backend/src/main/java/com/alttd/altitudeweb/services/chat/event_publisher/EventPublisher.java b/backend/src/main/java/com/alttd/altitudeweb/services/chat/event_publisher/EventPublisher.java index 4a0467f..c0d3417 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/services/chat/event_publisher/EventPublisher.java +++ b/backend/src/main/java/com/alttd/altitudeweb/services/chat/event_publisher/EventPublisher.java @@ -1,5 +1,10 @@ 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.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; @@ -7,6 +12,8 @@ import org.springframework.stereotype.Service; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.io.IOException; +import java.time.Duration; +import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -27,19 +34,10 @@ public class EventPublisher { emitterMap.computeIfAbsent(eventUser.uuid(), k -> new CopyOnWriteArrayList<>()).add(emitter); eventUserMap.put(eventUser.uuid(), eventUser); + Instant sessionStart = Instant.now(); log.info("User {} subscribed, active emitters: {}", eventUser.uuid(), countActive()); - Runnable cleanup = () -> { - List userEmitters = emitterMap.get(eventUser.uuid()); - if (userEmitters != null) { - userEmitters.remove(emitter); - if (userEmitters.isEmpty()) { - emitterMap.remove(eventUser.uuid()); - eventUserMap.remove(eventUser.uuid()); - } - } - log.info("Cleaned up emitter for {}, active emitters: {}", eventUser.uuid(), countActive()); - }; + Runnable cleanup = () -> handleCleanup(eventUser, emitter, sessionStart); // covers: clean disconnect (tab closed normally), server-side timeout, write failure emitter.onCompletion(cleanup); @@ -100,4 +98,39 @@ public class EventPublisher { sendToUser(emitter.uuid(), eventName, json); }); } + + private void handleCleanup(EventUser eventUser, SseEmitter emitter, Instant sessionStart) { + List userEmitters = emitterMap.get(eventUser.uuid()); + if (userEmitters != null) { + userEmitters.remove(emitter); + if (userEmitters.isEmpty()) { + emitterMap.remove(eventUser.uuid()); + eventUserMap.remove(eventUser.uuid()); + } + } + Instant sessionEnd = Instant.now(); + Duration between = Duration.between(sessionStart, sessionEnd); + log.info("Cleaned up emitter for {}, active emitters: {}, session duration: {}", + eventUser.uuid(), countActive(), between + ); + 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); + } + }); + } } diff --git a/database/src/main/java/com/alttd/altitudeweb/database/web_db/chat_session/ChatSession.java b/database/src/main/java/com/alttd/altitudeweb/database/web_db/chat_session/ChatSession.java new file mode 100644 index 0000000..ef60ba4 --- /dev/null +++ b/database/src/main/java/com/alttd/altitudeweb/database/web_db/chat_session/ChatSession.java @@ -0,0 +1,18 @@ +package com.alttd.altitudeweb.database.web_db.chat_session; + +import lombok.Builder; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.time.Instant; +import java.util.UUID; + +@Getter +@Builder +public class ChatSession { + + private final UUID uuid; + private final Instant session_start; + private final Instant session_end; + +} diff --git a/database/src/main/java/com/alttd/altitudeweb/database/web_db/chat_session/ChatSessionMapper.java b/database/src/main/java/com/alttd/altitudeweb/database/web_db/chat_session/ChatSessionMapper.java new file mode 100644 index 0000000..3942edb --- /dev/null +++ b/database/src/main/java/com/alttd/altitudeweb/database/web_db/chat_session/ChatSessionMapper.java @@ -0,0 +1,13 @@ +package com.alttd.altitudeweb.database.web_db.chat_session; + +import org.apache.ibatis.annotations.Insert; + +public interface ChatSessionMapper { + + @Insert(""" + INSERT INTO chat_session (uuid, session_start, session_end) + VALUES (#{uuid}, #{session_start}, #{session_end}) + """) + void storeSession(ChatSession chatSession); + +} diff --git a/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java b/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java index f5c1e1c..d8eb350 100644 --- a/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java +++ b/database/src/main/java/com/alttd/altitudeweb/setup/InitializeWebDb.java @@ -4,6 +4,7 @@ import com.alttd.altitudeweb.database.Databases; import com.alttd.altitudeweb.database.web_db.KeyPairMapper; import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper; import com.alttd.altitudeweb.database.web_db.SettingsMapper; +import com.alttd.altitudeweb.database.web_db.chat_session.ChatSessionMapper; import com.alttd.altitudeweb.database.web_db.forms.AppealMapper; import com.alttd.altitudeweb.database.web_db.forms.DiscordAppealMapper; import com.alttd.altitudeweb.database.web_db.forms.StaffApplicationMapper; @@ -28,6 +29,7 @@ public class InitializeWebDb { configuration.addMapper(DiscordAppealMapper.class); configuration.addMapper(StaffApplicationMapper.class); configuration.addMapper(EmailVerificationMapper.class); + configuration.addMapper(ChatSessionMapper.class); }).join() .runQuery(sqlSession -> { createSettingsTable(sqlSession); @@ -38,6 +40,7 @@ public class InitializeWebDb { createdDiscordAppealTable(sqlSession); createStaffApplicationsTable(sqlSession); createUserEmailsTable(sqlSession); + createChatSessionTable(sqlSession); }); log.debug("Initialized WebDb"); } @@ -208,4 +211,21 @@ public class InitializeWebDb { } } + private static void createChatSessionTable(@NotNull SqlSession sqlSession) { + String query = """ + CREATE TABLE IF NOT EXISTS chat_session ( + id int NOT NULL AUTO_INCREMENT PRIMARY KEY, + uuid UUID NOT NULL, + session_start TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + session_end TIMESTAMP NULL, + FOREIGN KEY (uuid) REFERENCES privileged_users(uuid) ON DELETE CASCADE ON UPDATE CASCADE + ); + """; + try (Statement statement = sqlSession.getConnection().createStatement()) { + statement.execute(query); + } catch (SQLException e) { + throw new RuntimeException(e); + } + } + }