Merge pull request #54 from duckietm/main

Sync Main to Dev
This commit is contained in:
DuckieTM
2026-03-29 14:48:57 +02:00
committed by GitHub
12 changed files with 313 additions and 36 deletions
+1
View File
@@ -0,0 +1 @@
ALTER TABLE `guild_forum_views` ADD UNIQUE KEY `user_guild` (`user_id`, `guild_id`);
@@ -210,6 +210,19 @@ public class ForumThread implements Runnable, ISerialize {
guildThreads.add(thread);
}
public static void clearCacheForGuild(int guildId) {
synchronized (guildThreadsCache) {
THashSet<ForumThread> threads = guildThreadsCache.remove(guildId);
if (threads != null) {
synchronized (forumThreadsCache) {
for (ForumThread thread : threads) {
forumThreadsCache.remove(thread.threadId);
}
}
}
}
}
public static void clearCache() {
for (THashSet<ForumThread> threads : guildThreadsCache.values()) {
for (ForumThread thread : threads) {
@@ -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);
@@ -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);
}
}
}
@@ -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<Guild> 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<Integer, long[]> myForumsCache = new ConcurrentHashMap<>(); // userId -> {cachedAt}
private static final ConcurrentHashMap<Integer, THashSet<Guild>> 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<Guild> getActiveForums() {
long now = System.currentTimeMillis();
if (activeForumsCache != null && (now - activeForumsCachedAt) < ACTIVE_FORUMS_TTL) {
return activeForumsCache;
}
THashSet<Guild> guilds = new THashSet<Guild>();
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<Guild> getMyForums(int userId) {
long now = System.currentTimeMillis();
long[] cached = myForumsCache.get(userId);
if (cached != null && (now - cached[0]) < MY_FORUMS_TTL) {
THashSet<Guild> data = myForumsData.get(userId);
if (data != null) return data;
}
THashSet<Guild> guilds = new THashSet<Guild>();
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;
}
}
}
@@ -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);
}
}
}
@@ -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;
}
@@ -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);
}
}
}
@@ -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));
@@ -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)));
@@ -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;
}
@@ -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<String, long[]> 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<String, long[]> 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<ForumThread> 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());