diff --git a/Database Updates/002_forum_groups.sql b/Database Updates/002_forum_groups.sql new file mode 100644 index 00000000..fe077955 --- /dev/null +++ b/Database Updates/002_forum_groups.sql @@ -0,0 +1 @@ +ALTER TABLE `guild_forum_views` ADD UNIQUE KEY `user_guild` (`user_id`, `guild_id`); \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java index f7d9a65e..0bc0c265 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/guilds/forums/ForumThread.java @@ -210,6 +210,19 @@ public class ForumThread implements Runnable, ISerialize { guildThreads.add(thread); } + public static void clearCacheForGuild(int guildId) { + synchronized (guildThreadsCache) { + THashSet threads = guildThreadsCache.remove(guildId); + if (threads != null) { + synchronized (forumThreadsCache) { + for (ForumThread thread : threads) { + forumThreadsCache.remove(thread.threadId); + } + } + } + } + } + public static void clearCache() { for (THashSet threads : guildThreadsCache.values()) { for (ForumThread thread : threads) { diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 885ec904..9f4efcc1 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -572,6 +572,7 @@ public class PacketManager { this.registerHandler(Incoming.GuildForumModerateMessageEvent, GuildForumModerateMessageEvent.class); this.registerHandler(Incoming.GuildForumModerateThreadEvent, GuildForumModerateThreadEvent.class); this.registerHandler(Incoming.GuildForumThreadUpdateEvent, GuildForumThreadUpdateEvent.class); + this.registerHandler(Incoming.GuildForumMarkAsReadEvent, GuildForumMarkAsReadEvent.class); this.registerHandler(Incoming.GetHabboGuildBadgesMessageEvent, GetHabboGuildBadgesMessageEvent.class); // this.registerHandler(Incoming.GuildForumDataEvent, GuildForumModerateMessageEvent.class); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java index bf99d98c..d46de9f2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/GuildChangeSettingsEvent.java @@ -6,9 +6,20 @@ import com.eu.habbo.habbohotel.guilds.GuildState; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.rooms.Room; import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.habbohotel.guilds.forums.ForumThread; +import com.eu.habbo.messages.incoming.guilds.forums.GuildForumListEvent; +import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; import com.eu.habbo.plugin.events.guilds.GuildChangedSettingsEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; public class GuildChangeSettingsEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(GuildChangeSettingsEvent.class); + @Override public int getRatelimit() { return 500; @@ -31,6 +42,24 @@ public class GuildChangeSettingsEvent extends MessageHandler { guild.setState(GuildState.valueOf(settingsEvent.state)); guild.setRights(settingsEvent.rights); + // Read forum toggle + boolean forumEnabled = this.packet.readBoolean(); + boolean wasForumEnabled = guild.hasForum(); + + if (forumEnabled != wasForumEnabled) { + guild.setForum(forumEnabled); + + if (!forumEnabled) { + // Delete all threads and comments for this guild + ForumThread.clearCacheForGuild(guildId); + deleteForumData(guildId); + } + + // Invalidate caches + GuildForumDataComposer.invalidateUnreadCache(guildId); + GuildForumListEvent.invalidateActiveForumsCache(); + } + Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(guild.getRoomId()); if(room != null) { room.refreshGuild(guild); @@ -42,4 +71,31 @@ public class GuildChangeSettingsEvent extends MessageHandler { } } } + + private void deleteForumData(int guildId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + // Delete comments for all threads in this guild + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM `guilds_forums_comments` WHERE `thread_id` IN (SELECT `id` FROM `guilds_forums_threads` WHERE `guild_id` = ?)")) { + statement.setInt(1, guildId); + statement.executeUpdate(); + } + + // Delete all threads for this guild + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM `guilds_forums_threads` WHERE `guild_id` = ?")) { + statement.setInt(1, guildId); + statement.executeUpdate(); + } + + // Delete forum view records for this guild + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM `guild_forum_views` WHERE `guild_id` = ?")) { + statement.setInt(1, guildId); + statement.executeUpdate(); + } + } catch (SQLException e) { + LOGGER.error("Failed to delete forum data for guild " + guildId, e); + } + } } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumListEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumListEvent.java index 83e7b484..aa76d1c8 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumListEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumListEvent.java @@ -14,6 +14,7 @@ import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; public class GuildForumListEvent extends MessageHandler { @Override @@ -23,6 +24,26 @@ public class GuildForumListEvent extends MessageHandler { private static final Logger LOGGER = LoggerFactory.getLogger(GuildForumListEvent.class); + // Cache for active forums list (shared across all users) + private static volatile THashSet activeForumsCache = null; + private static volatile long activeForumsCachedAt = 0; + private static final long ACTIVE_FORUMS_TTL = 30 * 60 * 1000; // 30 minutes + + // Cache for user's forum list + private static final ConcurrentHashMap myForumsCache = new ConcurrentHashMap<>(); // userId -> {cachedAt} + private static final ConcurrentHashMap> myForumsData = new ConcurrentHashMap<>(); + private static final long MY_FORUMS_TTL = 10 * 60 * 1000; // 10 minutes + + public static void invalidateActiveForumsCache() { + activeForumsCache = null; + activeForumsCachedAt = 0; + } + + public static void invalidateMyForumsCache(int userId) { + myForumsCache.remove(userId); + myForumsData.remove(userId); + } + @Override public void handle() throws Exception { int mode = this.packet.readInt(); @@ -50,12 +71,18 @@ public class GuildForumListEvent extends MessageHandler { } private THashSet getActiveForums() { + long now = System.currentTimeMillis(); + + if (activeForumsCache != null && (now - activeForumsCachedAt) < ACTIVE_FORUMS_TTL) { + return activeForumsCache; + } + THashSet guilds = new THashSet(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT `guilds`.`id`, SUM(`guilds_forums_threads`.`posts_count`) AS `post_count` " + "FROM `guilds_forums_threads` " + "LEFT JOIN `guilds` ON `guilds`.`id` = `guilds_forums_threads`.`guild_id` " + - "WHERE `guilds`.`read_forum` = 'EVERYONE' AND `guilds_forums_threads`.`created_at` > ? " + + "WHERE `guilds`.`forum` = '1' AND `guilds_forums_threads`.`created_at` > ? " + "GROUP BY `guilds`.`id` " + "ORDER BY `post_count` DESC LIMIT 100")) { statement.setInt(1, Emulator.getIntUnixTimestamp() - 7 * 24 * 60 * 60); @@ -73,10 +100,21 @@ public class GuildForumListEvent extends MessageHandler { this.client.sendResponse(new ConnectionErrorComposer(500)); } + activeForumsCache = guilds; + activeForumsCachedAt = now; + return guilds; } private THashSet getMyForums(int userId) { + long now = System.currentTimeMillis(); + + long[] cached = myForumsCache.get(userId); + if (cached != null && (now - cached[0]) < MY_FORUMS_TTL) { + THashSet data = myForumsData.get(userId); + if (data != null) return data; + } + THashSet guilds = new THashSet(); try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT `guilds`.`id` FROM `guilds_members` " + @@ -97,6 +135,9 @@ public class GuildForumListEvent extends MessageHandler { this.client.sendResponse(new ConnectionErrorComposer(500)); } + myForumsCache.put(userId, new long[]{now}); + myForumsData.put(userId, guilds); + return guilds; } -} \ No newline at end of file +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java new file mode 100644 index 00000000..0ea0f49d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumMarkAsReadEvent.java @@ -0,0 +1,50 @@ +package com.eu.habbo.messages.incoming.guilds.forums; + +import com.eu.habbo.Emulator; +import com.eu.habbo.messages.incoming.MessageHandler; +import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + +public class GuildForumMarkAsReadEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(GuildForumMarkAsReadEvent.class); + + @Override + public int getRatelimit() { + return 500; + } + + @Override + public void handle() throws Exception { + int count = this.packet.readInt(); + int userId = this.client.getHabbo().getHabboInfo().getId(); + int timestamp = Emulator.getIntUnixTimestamp(); + + for (int i = 0; i < count; i++) { + int guildId = this.packet.readInt(); + this.packet.readInt(); // messageId (not used, we track by timestamp) + this.packet.readBoolean(); // isRead + + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( + "INSERT INTO `guild_forum_views` (`user_id`, `guild_id`, `timestamp`) VALUES (?, ?, ?) " + + "ON DUPLICATE KEY UPDATE `timestamp` = ?" + )) { + statement.setInt(1, userId); + statement.setInt(2, guildId); + statement.setInt(3, timestamp); + statement.setInt(4, timestamp); + statement.executeUpdate(); + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + + // Invalidate caches so next request gets fresh data + GuildForumDataComposer.invalidateLastSeenCache(userId, guildId); + GuildForumDataComposer.invalidateUnreadCache(guildId); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateMessageEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateMessageEvent.java index bb4d87eb..6851d0fd 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateMessageEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateMessageEvent.java @@ -36,6 +36,11 @@ public class GuildForumModerateMessageEvent extends MessageHandler { return; } + if (thread.getGuildId() != guildId) { + this.client.sendResponse(new ConnectionErrorComposer(403)); + return; + } + ForumThreadComment comment = thread.getCommentById(messageId); if (comment == null) { this.client.sendResponse(new ConnectionErrorComposer(404)); @@ -45,19 +50,20 @@ public class GuildForumModerateMessageEvent extends MessageHandler { boolean hasStaffPermissions = this.client.getHabbo().hasPermission(Permission.ACC_MODTOOL_TICKET_Q); GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId()); - if (member == null) { + if (member == null && !hasStaffPermissions) { this.client.sendResponse(new ConnectionErrorComposer(401)); return; } - boolean isGuildAdministrator = (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || member.getRank().equals(GuildRank.ADMIN)); + boolean isGuildAdministrator = (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || (member != null && member.getRank().equals(GuildRank.ADMIN))); if (!isGuildAdministrator && !hasStaffPermissions) { this.client.sendResponse(new ConnectionErrorComposer(403)); return; } - if (state == ForumThreadState.HIDDEN_BY_GUILD_ADMIN.getStateId() && !hasStaffPermissions) { + // Restrict state 20 (staff hidden) to staff only + if (state == 20 && !hasStaffPermissions) { this.client.sendResponse(new ConnectionErrorComposer(403)); return; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateThreadEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateThreadEvent.java index 95a972fa..c5021046 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateThreadEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumModerateThreadEvent.java @@ -10,12 +10,21 @@ import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer; import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys; +import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumThreadMessagesComposer; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumThreadsComposer; import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; public class GuildForumModerateThreadEvent extends MessageHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(GuildForumModerateThreadEvent.class); + @Override public int getRatelimit() { return 500; @@ -26,8 +35,6 @@ public class GuildForumModerateThreadEvent extends MessageHandler { int guildId = packet.readInt(); int threadId = packet.readInt(); int state = packet.readInt(); - // STATE 20 - HIDDEN_BY_GUILD_ADMIN = HIDDEN BY GUILD ADMINS/ HOTEL MODERATORS - // STATE 1 = VISIBLE THREAD Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId); ForumThread thread = ForumThread.getById(threadId); @@ -37,6 +44,11 @@ public class GuildForumModerateThreadEvent extends MessageHandler { return; } + if (thread.getGuildId() != guildId) { + this.client.sendResponse(new ConnectionErrorComposer(403)); + return; + } + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId()); boolean hasStaffPerms = this.client.getHabbo().hasPermission(Permission.ACC_MODTOOL_TICKET_Q); @@ -52,12 +64,22 @@ public class GuildForumModerateThreadEvent extends MessageHandler { return; } - thread.setState(ForumThreadState.fromValue(state)); // sets state as defined in the packet + // State 20 = permanent delete (thread + comments removed from DB) + if (state == 20) { + deleteThread(threadId); + ForumThread.clearCacheForGuild(guildId); + GuildForumDataComposer.invalidateUnreadCache(guildId); + + this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_THREAD_HIDDEN.key).compose()); + this.client.sendResponse(new GuildForumThreadsComposer(guild, 0)); + return; + } + + thread.setState(ForumThreadState.fromValue(state)); thread.run(); switch (state) { case 10: - case 20: this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_THREAD_HIDDEN.key).compose()); break; case 1: @@ -68,4 +90,22 @@ public class GuildForumModerateThreadEvent extends MessageHandler { this.client.sendResponse(new GuildForumThreadMessagesComposer(thread)); this.client.sendResponse(new GuildForumThreadsComposer(guild, 0)); } + + private void deleteThread(int threadId) { + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) { + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM `guilds_forums_comments` WHERE `thread_id` = ?")) { + statement.setInt(1, threadId); + statement.executeUpdate(); + } + + try (PreparedStatement statement = connection.prepareStatement( + "DELETE FROM `guilds_forums_threads` WHERE `id` = ?")) { + statement.setInt(1, threadId); + statement.executeUpdate(); + } + } catch (SQLException e) { + LOGGER.error("Failed to delete thread " + threadId, e); + } + } } \ No newline at end of file diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumPostThreadEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumPostThreadEvent.java index a1730c3b..e5d213ac 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumPostThreadEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumPostThreadEvent.java @@ -9,6 +9,7 @@ import com.eu.habbo.habbohotel.guilds.forums.ForumThreadComment; import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.messages.incoming.MessageHandler; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumAddCommentComposer; +import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumDataComposer; import com.eu.habbo.messages.outgoing.guilds.forums.GuildForumThreadMessagesComposer; import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer; @@ -65,6 +66,7 @@ public class GuildForumPostThreadEvent extends MessageHandler { this.client.getHabbo().getHabboStats().forumPostsCount += 1; thread.setPostsCount(thread.getPostsCount() + 1); + GuildForumDataComposer.invalidateUnreadCache(guildId); this.client.sendResponse(new GuildForumThreadMessagesComposer(thread)); return; } @@ -74,6 +76,15 @@ public class GuildForumPostThreadEvent extends MessageHandler { return; } + if (thread.getGuildId() != guildId) { + this.client.sendResponse(new ConnectionErrorComposer(403)); + return; + } + + if (thread.isLocked() && !isStaff) { + this.client.sendResponse(new ConnectionErrorComposer(403)); + return; + } if (!((guild.canPostMessages().state == 0) || (guild.canPostMessages().state == 1 && member != null) @@ -91,6 +102,7 @@ public class GuildForumPostThreadEvent extends MessageHandler { thread.setUpdatedAt(Emulator.getIntUnixTimestamp()); this.client.getHabbo().getHabboStats().forumPostsCount += 1; thread.setPostsCount(thread.getPostsCount() + 1); + GuildForumDataComposer.invalidateUnreadCache(guildId); this.client.sendResponse(new GuildForumAddCommentComposer(comment)); } else { this.client.sendResponse(new ConnectionErrorComposer(500)); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsMessagesEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsMessagesEvent.java index 3fd6a50c..3680677f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsMessagesEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/guilds/forums/GuildForumThreadsMessagesEvent.java @@ -37,6 +37,13 @@ public class GuildForumThreadsMessagesEvent extends MessageHandler { this.client.sendResponse(new ConnectionErrorComposer(404)); return; } + + // Verify thread belongs to the requested guild + if (thread.getGuildId() != guildId) { + this.client.sendResponse(new ConnectionErrorComposer(403)); + return; + } + GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId()); boolean isGuildAdministrator = (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || (member != null && member.getRank().equals(GuildRank.ADMIN))); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/GuildManageComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/GuildManageComposer.java index 6c22192f..22619f34 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/GuildManageComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/GuildManageComposer.java @@ -62,6 +62,7 @@ public class GuildManageComposer extends MessageComposer { } this.response.appendString(this.guild.getBadge()); this.response.appendInt(this.guild.getMemberCount()); + this.response.appendBoolean(this.guild.hasForum()); return this.response; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/forums/GuildForumDataComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/forums/GuildForumDataComposer.java index d753f9a3..64abf8c3 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/forums/GuildForumDataComposer.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/guilds/forums/GuildForumDataComposer.java @@ -20,11 +20,20 @@ import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.concurrent.ConcurrentHashMap; public class GuildForumDataComposer extends MessageComposer { private static final Logger LOGGER = LoggerFactory.getLogger(GuildForumDataComposer.class); + // Cache for user last-seen timestamps: key = "userId:guildId", value = {timestamp, cachedAt} + private static final ConcurrentHashMap lastSeenCache = new ConcurrentHashMap<>(); + private static final long LAST_SEEN_CACHE_TTL = 5 * 60 * 1000; // 5 minutes + + // Cache for unread counts: key = "guildId:lastSeenAt", value = {count, cachedAt} + private static final ConcurrentHashMap unreadCache = new ConcurrentHashMap<>(); + private static final long UNREAD_CACHE_TTL = 2 * 60 * 1000; // 2 minutes + public final Guild guild; public Habbo habbo; @@ -33,13 +42,77 @@ public class GuildForumDataComposer extends MessageComposer { this.habbo = habbo; } + public static void invalidateLastSeenCache(int userId, int guildId) { + lastSeenCache.remove(userId + ":" + guildId); + } + + public static void invalidateUnreadCache(int guildId) { + unreadCache.entrySet().removeIf(entry -> entry.getKey().startsWith(guildId + ":")); + } + + private static int getLastSeenAt(int userId, int guildId) { + String key = userId + ":" + guildId; + long now = System.currentTimeMillis(); + + long[] cached = lastSeenCache.get(key); + if (cached != null && (now - cached[1]) < LAST_SEEN_CACHE_TTL) { + return (int) cached[0]; + } + + int lastSeenAt = 0; + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( + "SELECT `timestamp` FROM `guild_forum_views` WHERE `user_id` = ? AND `guild_id` = ? LIMIT 1" + )) { + statement.setInt(1, userId); + statement.setInt(2, guildId); + ResultSet set = statement.executeQuery(); + if (set.next()) { + lastSeenAt = set.getInt("timestamp"); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + + lastSeenCache.put(key, new long[]{lastSeenAt, now}); + return lastSeenAt; + } + + private static int getUnreadCount(int guildId, int lastSeenAt) { + String key = guildId + ":" + lastSeenAt; + long now = System.currentTimeMillis(); + + long[] cached = unreadCache.get(key); + if (cached != null && (now - cached[1]) < UNREAD_CACHE_TTL) { + return (int) cached[0]; + } + + int newComments = 0; + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( + "SELECT COUNT(*) FROM `guilds_forums_comments` " + + "JOIN `guilds_forums_threads` ON `guilds_forums_threads`.`id` = `guilds_forums_comments`.`thread_id` " + + "WHERE `guilds_forums_threads`.`guild_id` = ? AND `guilds_forums_comments`.`created_at` > ?" + )) { + statement.setInt(1, guildId); + statement.setInt(2, lastSeenAt); + + ResultSet set = statement.executeQuery(); + if (set.next()) { + newComments = set.getInt(1); + } + } catch (SQLException e) { + LOGGER.error("Caught SQL exception", e); + } + + unreadCache.put(key, new long[]{newComments, now}); + return newComments; + } + public static void serializeForumData(ServerMessage response, Guild guild, Habbo habbo) { final THashSet forumThreads = ForumThread.getByGuildId(guild.getId()); - int lastSeenAt = 0; + int lastSeenAt = getLastSeenAt(habbo.getHabboInfo().getId(), guild.getId()); int totalComments = 0; - int newComments = 0; int totalThreads = 0; ForumThreadComment lastComment = null; @@ -55,31 +128,7 @@ public class GuildForumDataComposer extends MessageComposer { } } - try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement( - "SELECT COUNT(*) " + - "FROM guilds_forums_threads A " + - "JOIN ( " + - "SELECT * " + - "FROM `guilds_forums_comments` " + - "WHERE `id` IN ( " + - "SELECT id " + - "FROM `guilds_forums_comments` B " + - "ORDER BY B.`id` ASC " + - ") " + - "ORDER BY `id` DESC " + - ") B ON A.`id` = B.`thread_id` " + - "WHERE A.`guild_id` = ? AND B.`created_at` > ?" - )) { - statement.setInt(1, guild.getId()); - statement.setInt(2, lastSeenAt); - - ResultSet set = statement.executeQuery(); - while (set.next()) { - newComments = set.getInt(1); - } - } catch (SQLException e) { - LOGGER.error("Caught SQL exception", e); - } + int newComments = getUnreadCount(guild.getId(), lastSeenAt); response.appendInt(guild.getId());