You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
Merge remote-tracking branch 'upstream/main' into feature/pr-20260327
This commit is contained in:
@@ -0,0 +1 @@
|
||||
ALTER TABLE `guild_forum_views` ADD UNIQUE KEY `user_guild` (`user_id`, `guild_id`);
|
||||
@@ -0,0 +1,6 @@
|
||||
INSERT IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('furni.editor.renderer.config.path', '/var/www/Gamedata/config/renderer-config.json'),
|
||||
('furni.editor.asset.base.path', '/var/www/Gamedata/furniture/nitro-assets/');
|
||||
|
||||
ALTER TABLE permissions
|
||||
ADD COLUMN `acc_catalogfurni` ENUM('0','1') NOT NULL DEFAULT '0' AFTER `acc_catalog_ids`;
|
||||
@@ -135,16 +135,10 @@ public class ForumThread implements Runnable, ISerialize {
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("SELECT A.*, B.`id` AS `last_comment_id` " +
|
||||
"FROM guilds_forums_threads A " +
|
||||
"JOIN (" +
|
||||
"SELECT * " +
|
||||
"LEFT JOIN (" +
|
||||
"SELECT `thread_id`, MAX(`id`) AS `id`, MAX(`created_at`) AS `created_at` " +
|
||||
"FROM `guilds_forums_comments` " +
|
||||
"WHERE `id` IN (" +
|
||||
"SELECT MAX(id) " +
|
||||
"FROM `guilds_forums_comments` B " +
|
||||
"GROUP BY `thread_id` AND B.`id` " +
|
||||
"ORDER BY B.`id` " +
|
||||
") " +
|
||||
"ORDER BY `id` DESC " +
|
||||
"GROUP BY `thread_id`" +
|
||||
") B ON A.`id` = B.`thread_id` " +
|
||||
"WHERE A.`guild_id` = ? " +
|
||||
"ORDER BY A.`pinned` DESC, B.`created_at` DESC "
|
||||
@@ -176,16 +170,10 @@ public class ForumThread implements Runnable, ISerialize {
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT A.*, B.`id` AS `last_comment_id` " +
|
||||
"FROM guilds_forums_threads A " +
|
||||
"JOIN (" +
|
||||
"SELECT * " +
|
||||
"LEFT JOIN (" +
|
||||
"SELECT `thread_id`, MAX(`id`) AS `id`, MAX(`created_at`) AS `created_at` " +
|
||||
"FROM `guilds_forums_comments` " +
|
||||
"WHERE `id` IN (" +
|
||||
"SELECT MAX(id) " +
|
||||
"FROM `guilds_forums_comments` B " +
|
||||
"GROUP BY `thread_id` AND b.`id`" +
|
||||
"ORDER BY B.`id` " +
|
||||
") " +
|
||||
"ORDER BY `id` DESC " +
|
||||
"GROUP BY `thread_id`" +
|
||||
") B ON A.`id` = B.`thread_id` " +
|
||||
"WHERE A.`id` = ? " +
|
||||
"ORDER BY A.`pinned` DESC, B.`created_at` DESC " +
|
||||
@@ -222,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) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.eu.habbo.messages.incoming.ambassadors.AmbassadorVisitCommandEvent;
|
||||
import com.eu.habbo.messages.incoming.camera.*;
|
||||
import com.eu.habbo.messages.incoming.catalog.*;
|
||||
import com.eu.habbo.messages.incoming.catalog.catalogadmin.*;
|
||||
import com.eu.habbo.messages.incoming.furnieditor.*;
|
||||
import com.eu.habbo.messages.incoming.catalog.marketplace.*;
|
||||
import com.eu.habbo.messages.incoming.catalog.recycler.OpenRecycleBoxEvent;
|
||||
import com.eu.habbo.messages.incoming.catalog.recycler.RecycleEvent;
|
||||
@@ -260,6 +261,14 @@ public class PacketManager {
|
||||
this.registerHandler(Incoming.CatalogRequestClubDiscountEvent, CatalogRequestClubDiscountEvent.class);
|
||||
this.registerHandler(Incoming.CatalogBuyClubDiscountEvent, CatalogBuyClubDiscountEvent.class);
|
||||
|
||||
// Furni Editor
|
||||
this.registerHandler(Incoming.FurniEditorSearchEvent, FurniEditorSearchEvent.class);
|
||||
this.registerHandler(Incoming.FurniEditorDetailEvent, FurniEditorDetailEvent.class);
|
||||
this.registerHandler(Incoming.FurniEditorBySpriteEvent, FurniEditorBySpriteEvent.class);
|
||||
this.registerHandler(Incoming.FurniEditorInteractionsEvent, FurniEditorInteractionsEvent.class);
|
||||
this.registerHandler(Incoming.FurniEditorUpdateEvent, FurniEditorUpdateEvent.class);
|
||||
this.registerHandler(Incoming.FurniEditorDeleteEvent, FurniEditorDeleteEvent.class);
|
||||
|
||||
// Catalog Admin
|
||||
this.registerHandler(Incoming.CatalogAdminSavePageEvent, CatalogAdminSavePageEvent.class);
|
||||
this.registerHandler(Incoming.CatalogAdminCreatePageEvent, CatalogAdminCreatePageEvent.class);
|
||||
@@ -572,6 +581,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);
|
||||
|
||||
@@ -412,6 +412,14 @@ public class Incoming {
|
||||
public static final int RequestInventoryPetDelete = 10030;
|
||||
public static final int RequestInventoryBadgeDelete = 10031;
|
||||
|
||||
// Furni Editor
|
||||
public static final int FurniEditorSearchEvent = 10040;
|
||||
public static final int FurniEditorDetailEvent = 10041;
|
||||
public static final int FurniEditorBySpriteEvent = 10042;
|
||||
public static final int FurniEditorInteractionsEvent = 10043;
|
||||
public static final int FurniEditorUpdateEvent = 10044;
|
||||
public static final int FurniEditorDeleteEvent = 10045;
|
||||
|
||||
// Catalog Admin
|
||||
public static final int CatalogAdminSavePageEvent = 10050;
|
||||
public static final int CatalogAdminCreatePageEvent = 10051;
|
||||
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
/**
|
||||
* Manages reading and writing of FurnitureData.json entries.
|
||||
* Resolves the file path from emulator config keys.
|
||||
*/
|
||||
public class FurniDataManager {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FurniDataManager.class);
|
||||
|
||||
/**
|
||||
* Get the JSON string for a specific item from FurnitureData.json.
|
||||
* Returns "{}" if not found or on error.
|
||||
*/
|
||||
public static String getItemJson(int itemId) {
|
||||
try {
|
||||
Path furniDataPath = resolveFurniDataPath();
|
||||
if (furniDataPath == null || !Files.exists(furniDataPath)) {
|
||||
return "{}";
|
||||
}
|
||||
|
||||
String content = Files.readString(furniDataPath, StandardCharsets.UTF_8);
|
||||
JsonObject root = JsonParser.parseString(content).getAsJsonObject();
|
||||
|
||||
// Search in both "roomitemtypes" and "wallitemtypes"
|
||||
for (String section : new String[]{"roomitemtypes", "wallitemtypes"}) {
|
||||
if (!root.has(section)) continue;
|
||||
JsonObject sectionObj = root.getAsJsonObject(section);
|
||||
if (!sectionObj.has("furnitype")) continue;
|
||||
JsonArray types = sectionObj.getAsJsonArray("furnitype");
|
||||
|
||||
for (JsonElement el : types) {
|
||||
JsonObject obj = el.getAsJsonObject();
|
||||
if (obj.has("id") && obj.get("id").getAsInt() == itemId) {
|
||||
return obj.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to read FurnitureData.json for item " + itemId, e);
|
||||
}
|
||||
|
||||
return "{}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the path to FurnitureData.json from emulator config.
|
||||
*/
|
||||
private static Path resolveFurniDataPath() {
|
||||
try {
|
||||
String configPath = Emulator.getConfig().getValue("furni.editor.renderer.config.path", "");
|
||||
|
||||
if (configPath.isEmpty()) {
|
||||
// Fallback: try common locations
|
||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||
if (!basePath.isEmpty()) {
|
||||
Path candidate = Paths.get(basePath, "FurnitureData.json");
|
||||
if (Files.exists(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Read the renderer config to find the furnidata URL/path
|
||||
Path rendererConfig = Paths.get(configPath);
|
||||
if (!Files.exists(rendererConfig)) return null;
|
||||
|
||||
String rendererContent = Files.readString(rendererConfig, StandardCharsets.UTF_8);
|
||||
JsonObject rendererObj = JsonParser.parseString(rendererContent).getAsJsonObject();
|
||||
|
||||
if (rendererObj.has("furnidata.url")) {
|
||||
String furniUrl = rendererObj.get("furnidata.url").getAsString();
|
||||
|
||||
// Skip unresolved placeholders like ${gamedata.url}
|
||||
if (furniUrl.contains("${")) {
|
||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||
if (!basePath.isEmpty()) {
|
||||
Path candidate = Paths.get(basePath, "FurnitureData.json");
|
||||
if (Files.exists(candidate)) return candidate;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Strip query string (?v=1 etc.)
|
||||
String cleanUrl = furniUrl.contains("?") ? furniUrl.substring(0, furniUrl.indexOf('?')) : furniUrl;
|
||||
|
||||
// If it's a local file path (not http), use it directly
|
||||
if (!cleanUrl.startsWith("http")) {
|
||||
return Paths.get(cleanUrl);
|
||||
}
|
||||
|
||||
// For http URLs, try to derive local path from base path
|
||||
String basePath = Emulator.getConfig().getValue("furni.editor.asset.base.path", "");
|
||||
if (!basePath.isEmpty()) {
|
||||
// Extract filename from URL (without query string)
|
||||
String filename = cleanUrl.substring(cleanUrl.lastIndexOf('/') + 1);
|
||||
return Paths.get(basePath, filename);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warn("Failed to resolve FurnitureData.json path", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
|
||||
public class FurniEditorBySpriteEvent extends MessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
int spriteId = this.packet.readInt();
|
||||
|
||||
if (spriteId <= 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid sprite ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Look up the item ID by sprite_id
|
||||
int itemId = -1;
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement stmt = connection.prepareStatement("SELECT id FROM items_base WHERE sprite_id = ? LIMIT 1")) {
|
||||
stmt.setInt(1, spriteId);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
itemId = rs.getInt("id");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (itemId <= 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No item found with sprite_id: " + spriteId));
|
||||
return;
|
||||
}
|
||||
|
||||
// Delegate to the detail response builder
|
||||
FurniEditorDetailEvent.sendDetailResponse(this.client, itemId);
|
||||
}
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
|
||||
public class FurniEditorDeleteEvent extends MessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
int id = this.packet.readInt();
|
||||
|
||||
if (id <= 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
// Check if item exists
|
||||
try (PreparedStatement stmt = connection.prepareStatement("SELECT id FROM items_base WHERE id = ?")) {
|
||||
stmt.setInt(1, id);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (!rs.next()) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Item not found: " + id));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check usage count - items placed in rooms
|
||||
int usageCount = 0;
|
||||
try (PreparedStatement stmt = connection.prepareStatement("SELECT COUNT(*) FROM items WHERE item_id = ?")) {
|
||||
stmt.setInt(1, id);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
usageCount = rs.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (usageCount > 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false,
|
||||
"Cannot delete: " + usageCount + " instances exist in the game"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Check catalog_items references
|
||||
int catalogCount = 0;
|
||||
try (PreparedStatement stmt = connection.prepareStatement(
|
||||
"SELECT COUNT(*) FROM catalog_items WHERE item_ids LIKE ?")) {
|
||||
stmt.setString(1, "%" + id + "%");
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
catalogCount = rs.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (catalogCount > 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false,
|
||||
"Cannot delete: item is referenced by " + catalogCount + " catalog entries"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Safe to delete
|
||||
try (PreparedStatement stmt = connection.prepareStatement("DELETE FROM items_base WHERE id = ?")) {
|
||||
stmt.setInt(1, id);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// Reload emulator item definitions
|
||||
Emulator.getGameEnvironment().getItemManager().loadItems();
|
||||
|
||||
this.client.sendResponse(new FurniEditorResultComposer(true, "Item deleted"));
|
||||
}
|
||||
}
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorDetailComposer;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class FurniEditorDetailEvent extends MessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
int id = this.packet.readInt();
|
||||
|
||||
if (id <= 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
sendDetailResponse(this.client, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared method to build and send a detail response for a given item ID.
|
||||
* Used by both FurniEditorDetailEvent and FurniEditorBySpriteEvent.
|
||||
*/
|
||||
public static void sendDetailResponse(com.eu.habbo.habbohotel.gameclients.GameClient client, int itemId) throws Exception {
|
||||
Map<String, Object> item = null;
|
||||
int usageCount = 0;
|
||||
List<Map<String, Object>> catalogItems = new ArrayList<>();
|
||||
String furniDataJson = "{}";
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
// Load full item data
|
||||
try (PreparedStatement stmt = connection.prepareStatement("SELECT * FROM items_base WHERE id = ?")) {
|
||||
stmt.setInt(1, itemId);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
item = FurniEditorHelper.readFullItem(rs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item == null) {
|
||||
client.sendResponse(new FurniEditorResultComposer(false, "Item not found: " + itemId));
|
||||
return;
|
||||
}
|
||||
|
||||
// Count placed instances
|
||||
try (PreparedStatement stmt = connection.prepareStatement("SELECT COUNT(*) FROM items WHERE item_id = ?")) {
|
||||
stmt.setInt(1, itemId);
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
usageCount = rs.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load catalog references (join catalog_items with catalog_pages)
|
||||
try (PreparedStatement stmt = connection.prepareStatement(
|
||||
"SELECT ci.id AS ci_id, ci.catalog_name, ci.cost_credits, ci.cost_points, ci.points_type, " +
|
||||
"ci.page_id AS ci_page_id, COALESCE(cp.caption, '') AS page_caption " +
|
||||
"FROM catalog_items ci " +
|
||||
"LEFT JOIN catalog_pages cp ON ci.page_id = cp.id " +
|
||||
"WHERE ci.item_ids LIKE ?")) {
|
||||
stmt.setString(1, "%" + itemId + "%");
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
catalogItems.add(FurniEditorHelper.readCatalogRef(rs));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try to read furnidata.json entry
|
||||
try {
|
||||
furniDataJson = FurniDataManager.getItemJson(itemId);
|
||||
} catch (Exception e) {
|
||||
furniDataJson = "{}";
|
||||
}
|
||||
|
||||
client.sendResponse(new FurniEditorDetailComposer(item, usageCount, catalogItems, furniDataJson));
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Shared utility for building item data maps from ResultSet rows.
|
||||
* Used by FurniEditorDetailEvent, FurniEditorBySpriteEvent, and
|
||||
* FurniEditorSearchEvent to ensure consistent field reading.
|
||||
*/
|
||||
public class FurniEditorHelper {
|
||||
|
||||
/**
|
||||
* Read the 14 base fields from items_base into a Map.
|
||||
*/
|
||||
public static Map<String, Object> readBaseItem(ResultSet set) throws SQLException {
|
||||
Map<String, Object> item = new HashMap<>();
|
||||
item.put("id", set.getInt("id"));
|
||||
item.put("sprite_id", set.getInt("sprite_id"));
|
||||
item.put("item_name", set.getString("item_name"));
|
||||
item.put("public_name", set.getString("public_name"));
|
||||
item.put("type", set.getString("type"));
|
||||
item.put("width", set.getInt("width"));
|
||||
item.put("length", set.getInt("length"));
|
||||
item.put("stack_height", set.getDouble("stack_height"));
|
||||
item.put("allow_stack", set.getString("allow_stack"));
|
||||
item.put("allow_walk", set.getString("allow_walk"));
|
||||
item.put("allow_sit", set.getString("allow_sit"));
|
||||
item.put("allow_lay", set.getString("allow_lay"));
|
||||
item.put("interaction_type", set.getString("interaction_type"));
|
||||
item.put("interaction_modes_count", set.getInt("interaction_modes_count"));
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all fields (14 base + 13 extended) from items_base into a Map.
|
||||
*/
|
||||
public static Map<String, Object> readFullItem(ResultSet set) throws SQLException {
|
||||
Map<String, Object> item = readBaseItem(set);
|
||||
item.put("allow_gift", set.getString("allow_gift"));
|
||||
item.put("allow_trade", set.getString("allow_trade"));
|
||||
item.put("allow_recycle", set.getString("allow_recycle"));
|
||||
item.put("allow_marketplace_sell", set.getString("allow_marketplace_sell"));
|
||||
item.put("allow_inventory_stack", set.getString("allow_inventory_stack"));
|
||||
item.put("vending_ids", set.getString("vending_ids"));
|
||||
item.put("customparams", set.getString("customparams"));
|
||||
item.put("effect_id_male", set.getInt("effect_id_male"));
|
||||
item.put("effect_id_female", set.getInt("effect_id_female"));
|
||||
item.put("clothing_on_walk", set.getString("clothing_on_walk"));
|
||||
item.put("multiheight", set.getString("multiheight"));
|
||||
|
||||
// description may not exist in all schemas, handle gracefully
|
||||
try {
|
||||
item.put("description", set.getString("description"));
|
||||
} catch (SQLException e) {
|
||||
item.put("description", "");
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a catalog item reference from a result set that joined
|
||||
* catalog_items with catalog_pages.
|
||||
*/
|
||||
public static Map<String, Object> readCatalogRef(ResultSet set) throws SQLException {
|
||||
Map<String, Object> ref = new HashMap<>();
|
||||
ref.put("id", set.getInt("ci_id"));
|
||||
ref.put("catalog_name", set.getString("catalog_name"));
|
||||
ref.put("cost_credits", set.getInt("cost_credits"));
|
||||
ref.put("cost_points", set.getInt("cost_points"));
|
||||
ref.put("points_type", set.getInt("points_type"));
|
||||
ref.put("page_id", set.getInt("ci_page_id"));
|
||||
ref.put("page_caption", set.getString("page_caption"));
|
||||
return ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whitelist of allowed field names for update operations.
|
||||
* Prevents SQL injection via arbitrary column names.
|
||||
*/
|
||||
public static final java.util.Set<String> ALLOWED_UPDATE_FIELDS = java.util.Set.of(
|
||||
"item_name", "public_name", "sprite_id", "type", "width", "length",
|
||||
"stack_height", "allow_stack", "allow_walk", "allow_sit", "allow_lay",
|
||||
"allow_gift", "allow_trade", "allow_recycle", "allow_marketplace_sell",
|
||||
"allow_inventory_stack", "interaction_type", "interaction_modes_count",
|
||||
"vending_ids", "customparams", "effect_id_male", "effect_id_female",
|
||||
"clothing_on_walk", "multiheight", "description"
|
||||
);
|
||||
|
||||
/**
|
||||
* Map camelCase JS field names to DB column names.
|
||||
*/
|
||||
public static final Map<String, String> FIELD_MAP = Map.ofEntries(
|
||||
Map.entry("itemName", "item_name"),
|
||||
Map.entry("publicName", "public_name"),
|
||||
Map.entry("spriteId", "sprite_id"),
|
||||
Map.entry("type", "type"),
|
||||
Map.entry("width", "width"),
|
||||
Map.entry("length", "length"),
|
||||
Map.entry("stackHeight", "stack_height"),
|
||||
Map.entry("allowStack", "allow_stack"),
|
||||
Map.entry("allowWalk", "allow_walk"),
|
||||
Map.entry("allowSit", "allow_sit"),
|
||||
Map.entry("allowLay", "allow_lay"),
|
||||
Map.entry("allowGift", "allow_gift"),
|
||||
Map.entry("allowTrade", "allow_trade"),
|
||||
Map.entry("allowRecycle", "allow_recycle"),
|
||||
Map.entry("allowMarketplaceSell", "allow_marketplace_sell"),
|
||||
Map.entry("allowInventoryStack", "allow_inventory_stack"),
|
||||
Map.entry("interactionType", "interaction_type"),
|
||||
Map.entry("interactionModesCount", "interaction_modes_count"),
|
||||
Map.entry("vendingIds", "vending_ids"),
|
||||
Map.entry("customparams", "customparams"),
|
||||
Map.entry("effectIdMale", "effect_id_male"),
|
||||
Map.entry("effectIdFemale", "effect_id_female"),
|
||||
Map.entry("clothingOnWalk", "clothing_on_walk"),
|
||||
Map.entry("multiheight", "multiheight"),
|
||||
Map.entry("description", "description")
|
||||
);
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorInteractionsComposer;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class FurniEditorInteractionsEvent extends MessageHandler {
|
||||
|
||||
private static List<String> cachedInteractions = null;
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (cachedInteractions == null) {
|
||||
synchronized (FurniEditorInteractionsEvent.class) {
|
||||
if (cachedInteractions == null) {
|
||||
List<String> list = new ArrayList<>();
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
Statement stmt = connection.createStatement();
|
||||
ResultSet set = stmt.executeQuery("SELECT DISTINCT interaction_type FROM items_base WHERE interaction_type != '' ORDER BY interaction_type ASC")) {
|
||||
while (set.next()) {
|
||||
list.add(set.getString("interaction_type"));
|
||||
}
|
||||
}
|
||||
cachedInteractions = Collections.unmodifiableList(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.client.sendResponse(new FurniEditorInteractionsComposer(cachedInteractions));
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorSearchComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class FurniEditorSearchEvent extends MessageHandler {
|
||||
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
String query = this.packet.readString();
|
||||
String type = this.packet.readString();
|
||||
int page = this.packet.readInt();
|
||||
|
||||
// Input validation
|
||||
if (query.length() > 100) {
|
||||
query = query.substring(0, 100);
|
||||
}
|
||||
|
||||
if (page < 1) page = 1;
|
||||
|
||||
int offset = (page - 1) * PAGE_SIZE;
|
||||
|
||||
// Build WHERE clause
|
||||
StringBuilder whereClause = new StringBuilder("WHERE 1=1");
|
||||
List<Object> params = new ArrayList<>();
|
||||
|
||||
if (!query.isEmpty()) {
|
||||
// Try numeric match first (id or sprite_id)
|
||||
boolean isNumeric = false;
|
||||
try {
|
||||
int numericQuery = Integer.parseInt(query);
|
||||
isNumeric = true;
|
||||
whereClause.append(" AND (id = ? OR sprite_id = ? OR item_name LIKE ? OR public_name LIKE ?)");
|
||||
params.add(numericQuery);
|
||||
params.add(numericQuery);
|
||||
params.add("%" + query + "%");
|
||||
params.add("%" + query + "%");
|
||||
} catch (NumberFormatException e) {
|
||||
whereClause.append(" AND (item_name LIKE ? OR public_name LIKE ?)");
|
||||
params.add("%" + query + "%");
|
||||
params.add("%" + query + "%");
|
||||
}
|
||||
}
|
||||
|
||||
if (type != null && !type.isEmpty()) {
|
||||
whereClause.append(" AND type = ?");
|
||||
params.add(type);
|
||||
}
|
||||
|
||||
// Count total
|
||||
int total = 0;
|
||||
String countSql = "SELECT COUNT(*) FROM items_base " + whereClause;
|
||||
String dataSql = "SELECT * FROM items_base " + whereClause + " ORDER BY id ASC LIMIT ? OFFSET ?";
|
||||
|
||||
List<Map<String, Object>> items = new ArrayList<>();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection()) {
|
||||
// Get total count
|
||||
try (PreparedStatement stmt = connection.prepareStatement(countSql)) {
|
||||
int idx = 1;
|
||||
for (Object param : params) {
|
||||
if (param instanceof Integer) {
|
||||
stmt.setInt(idx++, (Integer) param);
|
||||
} else {
|
||||
stmt.setString(idx++, (String) param);
|
||||
}
|
||||
}
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
if (rs.next()) {
|
||||
total = rs.getInt(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get items page
|
||||
try (PreparedStatement stmt = connection.prepareStatement(dataSql)) {
|
||||
int idx = 1;
|
||||
for (Object param : params) {
|
||||
if (param instanceof Integer) {
|
||||
stmt.setInt(idx++, (Integer) param);
|
||||
} else {
|
||||
stmt.setString(idx++, (String) param);
|
||||
}
|
||||
}
|
||||
stmt.setInt(idx++, PAGE_SIZE);
|
||||
stmt.setInt(idx, offset);
|
||||
|
||||
try (ResultSet rs = stmt.executeQuery()) {
|
||||
while (rs.next()) {
|
||||
items.add(FurniEditorHelper.readBaseItem(rs));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.client.sendResponse(new FurniEditorSearchComposer(items, total, page));
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package com.eu.habbo.messages.incoming.furnieditor;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.permissions.Permission;
|
||||
import com.eu.habbo.messages.incoming.MessageHandler;
|
||||
import com.eu.habbo.messages.outgoing.furnieditor.FurniEditorResultComposer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class FurniEditorUpdateEvent extends MessageHandler {
|
||||
|
||||
@Override
|
||||
public void handle() throws Exception {
|
||||
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No permission"));
|
||||
return;
|
||||
}
|
||||
|
||||
int id = this.packet.readInt();
|
||||
String jsonFieldsStr = this.packet.readString();
|
||||
|
||||
if (id <= 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid item ID"));
|
||||
return;
|
||||
}
|
||||
|
||||
JsonObject json;
|
||||
try {
|
||||
json = JsonParser.parseString(jsonFieldsStr).getAsJsonObject();
|
||||
} catch (Exception e) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "Invalid JSON data"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (json.size() == 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No fields to update"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Build dynamic UPDATE with whitelisted fields
|
||||
StringBuilder setClauses = new StringBuilder();
|
||||
List<Object> values = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, JsonElement> entry : json.entrySet()) {
|
||||
String jsKey = entry.getKey();
|
||||
String dbColumn = FurniEditorHelper.FIELD_MAP.get(jsKey);
|
||||
|
||||
if (dbColumn == null || !FurniEditorHelper.ALLOWED_UPDATE_FIELDS.contains(dbColumn)) {
|
||||
continue; // Skip unknown or disallowed fields
|
||||
}
|
||||
|
||||
if (setClauses.length() > 0) setClauses.append(", ");
|
||||
setClauses.append("`").append(dbColumn).append("` = ?");
|
||||
|
||||
JsonElement val = entry.getValue();
|
||||
if (val.isJsonPrimitive()) {
|
||||
if (val.getAsJsonPrimitive().isBoolean()) {
|
||||
values.add(val.getAsBoolean() ? "1" : "0");
|
||||
} else if (val.getAsJsonPrimitive().isNumber()) {
|
||||
// Check if it's a decimal number
|
||||
String numStr = val.getAsString();
|
||||
if (numStr.contains(".")) {
|
||||
values.add(val.getAsDouble());
|
||||
} else {
|
||||
values.add(val.getAsInt());
|
||||
}
|
||||
} else {
|
||||
values.add(val.getAsString());
|
||||
}
|
||||
} else {
|
||||
values.add(val.toString());
|
||||
}
|
||||
}
|
||||
|
||||
if (setClauses.length() == 0) {
|
||||
this.client.sendResponse(new FurniEditorResultComposer(false, "No valid fields to update"));
|
||||
return;
|
||||
}
|
||||
|
||||
String sql = "UPDATE items_base SET " + setClauses + " WHERE id = ?";
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement stmt = connection.prepareStatement(sql)) {
|
||||
int idx = 1;
|
||||
for (Object value : values) {
|
||||
if (value instanceof Integer) {
|
||||
stmt.setInt(idx++, (Integer) value);
|
||||
} else if (value instanceof Double) {
|
||||
stmt.setDouble(idx++, (Double) value);
|
||||
} else {
|
||||
stmt.setString(idx++, String.valueOf(value));
|
||||
}
|
||||
}
|
||||
stmt.setInt(idx, id);
|
||||
stmt.executeUpdate();
|
||||
}
|
||||
|
||||
// Reload emulator item definitions
|
||||
Emulator.getGameEnvironment().getItemManager().loadItems();
|
||||
|
||||
this.client.sendResponse(new FurniEditorResultComposer(true, "Item updated", id));
|
||||
}
|
||||
}
|
||||
+78
-1
@@ -2,16 +2,34 @@ package com.eu.habbo.messages.incoming.guilds;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.guilds.Guild;
|
||||
import com.eu.habbo.habbohotel.guilds.GuildMember;
|
||||
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.GuildInfoComposer;
|
||||
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;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class GuildChangeSettingsEvent extends MessageHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(GuildChangeSettingsEvent.class);
|
||||
|
||||
// Cooldown for forum toggle per guild: guildId -> last toggle timestamp
|
||||
private static final ConcurrentHashMap<Integer, Long> forumToggleCooldown = new ConcurrentHashMap<>();
|
||||
private static final long FORUM_TOGGLE_COOLDOWN_MS = 30_000; // 30 seconds
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
return 2000; // 2 seconds between settings saves
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -31,6 +49,34 @@ 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) {
|
||||
// Enforce cooldown on forum toggle to prevent rapid enable/disable spam
|
||||
Long lastToggle = forumToggleCooldown.get(guildId);
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (lastToggle != null && (now - lastToggle) < FORUM_TOGGLE_COOLDOWN_MS) {
|
||||
LOGGER.warn("Forum toggle cooldown for guild {} by user {}", guildId, this.client.getHabbo().getHabboInfo().getUsername());
|
||||
} else {
|
||||
forumToggleCooldown.put(guildId, now);
|
||||
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();
|
||||
GuildForumListEvent.invalidateMyForumsCache(this.client.getHabbo().getHabboInfo().getId());
|
||||
}
|
||||
}
|
||||
|
||||
Room room = Emulator.getGameEnvironment().getRoomManager().getRoom(guild.getRoomId());
|
||||
if(room != null) {
|
||||
room.refreshGuild(guild);
|
||||
@@ -39,7 +85,38 @@ public class GuildChangeSettingsEvent extends MessageHandler {
|
||||
guild.needsUpdate = true;
|
||||
|
||||
Emulator.getThreading().run(guild);
|
||||
|
||||
// Send updated group info back to client so hasForum flag refreshes immediately
|
||||
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
|
||||
this.client.sendResponse(new GuildInfoComposer(guild, this.client, false, member));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+43
-2
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
@@ -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 2000;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+10
-4
@@ -18,7 +18,7 @@ import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer;
|
||||
public class GuildForumModerateMessageEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+45
-5
@@ -10,15 +10,24 @@ 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;
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -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;
|
||||
|
||||
@@ -17,7 +18,7 @@ public class GuildForumPostThreadEvent extends MessageHandler {
|
||||
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 1000;
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -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));
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer;
|
||||
public class GuildForumThreadUpdateEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ public class GuildForumThreadsEvent extends MessageHandler {
|
||||
|
||||
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
|
||||
|
||||
if (guild == null || !guild.hasForum()) {
|
||||
if (guild == null) {
|
||||
this.client.sendResponse(new ConnectionErrorComposer(404));
|
||||
return;
|
||||
}
|
||||
|
||||
+7
@@ -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)));
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer;
|
||||
public class GuildForumUpdateSettingsEvent extends MessageHandler {
|
||||
@Override
|
||||
public int getRatelimit() {
|
||||
return 500;
|
||||
return 2000;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -556,6 +556,12 @@ public class Outgoing {
|
||||
public static final int SnowStormUserRematchedComposer = 5029;
|
||||
|
||||
|
||||
// Furni Editor
|
||||
public static final int FurniEditorSearchComposer = 10040;
|
||||
public static final int FurniEditorDetailComposer = 10041;
|
||||
public static final int FurniEditorInteractionsComposer = 10043;
|
||||
public static final int FurniEditorResultComposer = 10044;
|
||||
|
||||
// Catalog Admin
|
||||
public static final int CatalogAdminResultComposer = 10059;
|
||||
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
package com.eu.habbo.messages.outgoing.furnieditor;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class FurniEditorDetailComposer extends MessageComposer {
|
||||
private final Map<String, Object> item;
|
||||
private final int usageCount;
|
||||
private final List<Map<String, Object>> catalogItems;
|
||||
private final String furniDataJson;
|
||||
|
||||
public FurniEditorDetailComposer(Map<String, Object> item, int usageCount, List<Map<String, Object>> catalogItems, String furniDataJson) {
|
||||
this.item = item;
|
||||
this.usageCount = usageCount;
|
||||
this.catalogItems = catalogItems;
|
||||
this.furniDataJson = furniDataJson;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.FurniEditorDetailComposer);
|
||||
|
||||
// 14 base fields
|
||||
this.response.appendInt((int) item.get("id"));
|
||||
this.response.appendInt((int) item.get("sprite_id"));
|
||||
this.response.appendString((String) item.getOrDefault("item_name", ""));
|
||||
this.response.appendString((String) item.getOrDefault("public_name", ""));
|
||||
this.response.appendString((String) item.getOrDefault("type", "s"));
|
||||
this.response.appendInt((int) item.getOrDefault("width", 1));
|
||||
this.response.appendInt((int) item.getOrDefault("length", 1));
|
||||
this.response.appendDouble((double) item.getOrDefault("stack_height", 0.0));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_stack", "1"))));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_walk", "0"))));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_sit", "0"))));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_lay", "0"))));
|
||||
this.response.appendString((String) item.getOrDefault("interaction_type", ""));
|
||||
this.response.appendInt((int) item.getOrDefault("interaction_modes_count", 0));
|
||||
|
||||
// 13 extended fields
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_gift", "1"))));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_trade", "1"))));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_recycle", "1"))));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_marketplace_sell", "1"))));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_inventory_stack", "1"))));
|
||||
this.response.appendString((String) item.getOrDefault("vending_ids", ""));
|
||||
this.response.appendString((String) item.getOrDefault("customparams", ""));
|
||||
this.response.appendInt((int) item.getOrDefault("effect_id_male", 0));
|
||||
this.response.appendInt((int) item.getOrDefault("effect_id_female", 0));
|
||||
this.response.appendString((String) item.getOrDefault("clothing_on_walk", ""));
|
||||
this.response.appendString((String) item.getOrDefault("multiheight", ""));
|
||||
this.response.appendString((String) item.getOrDefault("description", ""));
|
||||
|
||||
// usage count
|
||||
this.response.appendInt(this.usageCount);
|
||||
|
||||
// catalog references
|
||||
this.response.appendInt(this.catalogItems.size());
|
||||
for (Map<String, Object> ci : this.catalogItems) {
|
||||
this.response.appendInt((int) ci.get("id"));
|
||||
this.response.appendString((String) ci.getOrDefault("catalog_name", ""));
|
||||
this.response.appendInt((int) ci.getOrDefault("cost_credits", 0));
|
||||
this.response.appendInt((int) ci.getOrDefault("cost_points", 0));
|
||||
this.response.appendInt((int) ci.getOrDefault("points_type", 0));
|
||||
this.response.appendInt((int) ci.getOrDefault("page_id", -1));
|
||||
this.response.appendString((String) ci.getOrDefault("page_caption", ""));
|
||||
}
|
||||
|
||||
// furnidata JSON string
|
||||
this.response.appendString(this.furniDataJson != null ? this.furniDataJson : "{}");
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package com.eu.habbo.messages.outgoing.furnieditor;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class FurniEditorInteractionsComposer extends MessageComposer {
|
||||
private final List<String> interactions;
|
||||
|
||||
public FurniEditorInteractionsComposer(List<String> interactions) {
|
||||
this.interactions = interactions;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.FurniEditorInteractionsComposer);
|
||||
this.response.appendInt(this.interactions.size());
|
||||
|
||||
for (String interaction : this.interactions) {
|
||||
this.response.appendString(interaction);
|
||||
}
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package com.eu.habbo.messages.outgoing.furnieditor;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
public class FurniEditorResultComposer extends MessageComposer {
|
||||
private final boolean success;
|
||||
private final String message;
|
||||
private final int id;
|
||||
|
||||
public FurniEditorResultComposer(boolean success, String message) {
|
||||
this(success, message, -1);
|
||||
}
|
||||
|
||||
public FurniEditorResultComposer(boolean success, String message, int id) {
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.FurniEditorResultComposer);
|
||||
this.response.appendBoolean(this.success);
|
||||
this.response.appendString(this.message);
|
||||
this.response.appendInt(this.id);
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package com.eu.habbo.messages.outgoing.furnieditor;
|
||||
|
||||
import com.eu.habbo.messages.ServerMessage;
|
||||
import com.eu.habbo.messages.outgoing.MessageComposer;
|
||||
import com.eu.habbo.messages.outgoing.Outgoing;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class FurniEditorSearchComposer extends MessageComposer {
|
||||
private final List<Map<String, Object>> items;
|
||||
private final int total;
|
||||
private final int page;
|
||||
|
||||
public FurniEditorSearchComposer(List<Map<String, Object>> items, int total, int page) {
|
||||
this.items = items;
|
||||
this.total = total;
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ServerMessage composeInternal() {
|
||||
this.response.init(Outgoing.FurniEditorSearchComposer);
|
||||
this.response.appendInt(this.items.size());
|
||||
|
||||
for (Map<String, Object> item : this.items) {
|
||||
this.response.appendInt((int) item.get("id"));
|
||||
this.response.appendInt((int) item.get("sprite_id"));
|
||||
this.response.appendString((String) item.getOrDefault("item_name", ""));
|
||||
this.response.appendString((String) item.getOrDefault("public_name", ""));
|
||||
this.response.appendString((String) item.getOrDefault("type", "s"));
|
||||
this.response.appendInt((int) item.getOrDefault("width", 1));
|
||||
this.response.appendInt((int) item.getOrDefault("length", 1));
|
||||
this.response.appendDouble((double) item.getOrDefault("stack_height", 0.0));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_stack", "1"))));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_walk", "0"))));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_sit", "0"))));
|
||||
this.response.appendBoolean("1".equals(String.valueOf(item.getOrDefault("allow_lay", "0"))));
|
||||
this.response.appendString((String) item.getOrDefault("interaction_type", ""));
|
||||
this.response.appendInt((int) item.getOrDefault("interaction_modes_count", 0));
|
||||
}
|
||||
|
||||
this.response.appendInt(this.total);
|
||||
this.response.appendInt(this.page);
|
||||
|
||||
return this.response;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+76
-27
@@ -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());
|
||||
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user