Merge pull request #116 from duckietm/dev

Dev
This commit is contained in:
DuckieTM
2026-05-21 09:01:07 +02:00
committed by GitHub
8 changed files with 181 additions and 22 deletions
@@ -0,0 +1,31 @@
-- ============================================================
-- Fix: acc_supporttool wrongly granted to VIP / wrongly denied to Super Mod
-- ============================================================
-- The default permission_definitions seed shipped acc_supporttool
-- with rank pattern (0, 1, 1, 1, 1, 0, 1) — i.e. rank_2 (VIP) and
-- rank_3 (X, junior helper) had ALLOWED, while rank_6 (Super Mod)
-- did NOT. That's two bugs:
--
-- * VIP users see the ModTools button on the toolbar and can
-- open Room/User info windows. The actual sanction endpoints
-- still gate on ACC_SUPPORTTOOL server-side so they can't
-- actually moderate, but the UI exposure is wrong and lets a
-- VIP request user info / room info / chatlogs they have no
-- business reading.
-- * Super Mod is denied the tool entirely, which is obviously
-- unintended given the rank name.
--
-- Intended pattern: only Support (4) and up — (0, 0, 0, 1, 1, 1, 1).
--
-- Run on existing deployments to align with the corrected default
-- seed in `Default Database/FullDatabase.sql`. Idempotent.
UPDATE `permission_definitions`
SET `rank_1` = 0,
`rank_2` = 0,
`rank_3` = 0,
`rank_4` = 1,
`rank_5` = 1,
`rank_6` = 1,
`rank_7` = 1
WHERE `permission_key` = 'acc_supporttool';
+1 -1
View File
@@ -28598,7 +28598,7 @@ INSERT INTO `permission_definitions` (`permission_key`, `max_value`, `comment`,
('acc_staff_chat', 1, 'Grants access to the in-game Staff Chat group buddy: receives broadcasts from other staff and can broadcast to anyone holding this permission.', 0, 0, 0, 0, 0, 0, 1), ('acc_staff_chat', 1, 'Grants access to the in-game Staff Chat group buddy: receives broadcasts from other staff and can broadcast to anyone holding this permission.', 0, 0, 0, 0, 0, 0, 1),
('acc_staff_pick', 1, 'Allows using staff item pick-up actions that bypass normal room ownership restrictions.', 0, 0, 0, 0, 0, 0, 1), ('acc_staff_pick', 1, 'Allows using staff item pick-up actions that bypass normal room ownership restrictions.', 0, 0, 0, 0, 0, 0, 1),
('acc_superwired', 1, 'Allows saving advanced wired data without the normal wordfilter and reward payload restrictions applied to regular users.', 0, 0, 0, 0, 0, 0, 1), ('acc_superwired', 1, 'Allows saving advanced wired data without the normal wordfilter and reward payload restrictions applied to regular users.', 0, 0, 0, 0, 0, 0, 1),
('acc_supporttool', 1, 'Allows opening and using the support/moderation tool interface.', 0, 1, 1, 1, 1, 0, 1), ('acc_supporttool', 1, 'Allows opening and using the support/moderation tool interface.', 0, 0, 0, 1, 1, 1, 1),
('acc_trade_anywhere', 1, 'Allows starting trades outside the normal trade-enabled areas.', 0, 0, 0, 0, 0, 0, 1), ('acc_trade_anywhere', 1, 'Allows starting trades outside the normal trade-enabled areas.', 0, 0, 0, 0, 0, 0, 1),
('acc_unignorable', 1, 'Prevents the account from being ignored by other users through the ignore system.', 0, 0, 0, 0, 0, 0, 0), ('acc_unignorable', 1, 'Prevents the account from being ignored by other users through the ignore system.', 0, 0, 0, 0, 0, 0, 0),
('acc_unkickable', 1, 'Prevents the user from being kicked by normal moderation or room commands.', 0, 0, 0, 0, 0, 0, 1), ('acc_unkickable', 1, 'Prevents the user from being kicked by normal moderation or room commands.', 0, 0, 0, 0, 0, 0, 1),
@@ -651,6 +651,10 @@ public class ModToolManager {
sender.getClient().sendResponse(new ModToolIssueHandledComposer(ModToolIssueHandledComposer.ABUSIVE)); sender.getClient().sendResponse(new ModToolIssueHandledComposer(ModToolIssueHandledComposer.ABUSIVE));
} }
// Reporter (the user who opened the CFH) gets their abusive
// counter bumped — the legacy stat shown in the User Info table.
bumpUserSettingCounter(issue.senderId, "cfh_abusive");
this.updateTicketToMods(issue); this.updateTicketToMods(issue);
this.removeTicket(issue); this.removeTicket(issue);
@@ -737,4 +741,38 @@ public class ModToolManager {
return issues; return issues;
} }
/**
* Increments a single integer counter on `users_settings` for the
* given user. Used by the moderation sanction handlers to bump the
* legacy counters that `ModToolUserInfoComposer` surfaces (cfh_warnings,
* cfh_bans, cfh_abusive, tradelock_amount) — historically these were
* only ever incremented by the CFH submission path, so a user could
* accumulate any number of bans/mutes without the User Info table
* reflecting it.
*
* Restricted to a whitelisted column name to keep the dynamic SQL
* safe; the caller passes a Permission-style constant.
*/
public static void bumpUserSettingCounter(int userId, String column) {
switch (column) {
case "cfh_warnings":
case "cfh_bans":
case "cfh_abusive":
case "tradelock_amount":
break;
default:
LOGGER.warn("Refusing to bump unrecognized user_settings column: {}", column);
return;
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE users_settings SET " + column + " = " + column + " + 1 WHERE user_id = ?")) {
statement.setInt(1, userId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception bumping {} for user {}", column, userId, e);
}
}
} }
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.incoming.modtool; package com.eu.habbo.messages.incoming.modtool;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ModToolManager;
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem; import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
import com.eu.habbo.habbohotel.modtool.ModToolSanctions; import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.permissions.Permission;
@@ -47,6 +48,8 @@ public class ModToolSanctionAlertEvent extends MessageHandler {
} else { } else {
habbo.alert(message); habbo.alert(message);
} }
ModToolManager.bumpUserSettingCounter(userId, "cfh_warnings");
} else { } else {
this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name")))); this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name"))));
} }
@@ -2,6 +2,7 @@ package com.eu.habbo.messages.incoming.modtool;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ModToolBanType; import com.eu.habbo.habbohotel.modtool.ModToolBanType;
import com.eu.habbo.habbohotel.modtool.ModToolManager;
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem; import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
import com.eu.habbo.habbohotel.modtool.ModToolSanctions; import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
import com.eu.habbo.habbohotel.modtool.ScripterManager; import com.eu.habbo.habbohotel.modtool.ScripterManager;
@@ -73,6 +74,7 @@ public class ModToolSanctionBanEvent extends MessageHandler {
Emulator.getGameEnvironment().getModToolManager().ban(userId, this.client.getHabbo(), message, duration, ModToolBanType.ACCOUNT, cfhTopic); Emulator.getGameEnvironment().getModToolManager().ban(userId, this.client.getHabbo(), message, duration, ModToolBanType.ACCOUNT, cfhTopic);
} }
ModToolManager.bumpUserSettingCounter(userId, "cfh_bans");
} else { } else {
ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.modtools.ban").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername())); ScripterManager.scripterDetected(this.client, Emulator.getTexts().getValue("scripter.warning.modtools.ban").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()));
} }
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.incoming.modtool; package com.eu.habbo.messages.incoming.modtool;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ModToolManager;
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem; import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
import com.eu.habbo.habbohotel.modtool.ModToolSanctionLevelItem; import com.eu.habbo.habbohotel.modtool.ModToolSanctionLevelItem;
import com.eu.habbo.habbohotel.modtool.ModToolSanctions; import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
@@ -59,6 +60,8 @@ public class ModToolSanctionMuteEvent extends MessageHandler {
habbo.alert(message); habbo.alert(message);
this.client.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_mute.muted").replace("%user%", habbo.getHabboInfo().getUsername())); this.client.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_mute.muted").replace("%user%", habbo.getHabboInfo().getUsername()));
} }
ModToolManager.bumpUserSettingCounter(userId, "cfh_warnings");
} else { } else {
this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name")))); this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name"))));
} }
@@ -1,6 +1,7 @@
package com.eu.habbo.messages.incoming.modtool; package com.eu.habbo.messages.incoming.modtool;
import com.eu.habbo.Emulator; import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.ModToolManager;
import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem; import com.eu.habbo.habbohotel.modtool.ModToolSanctionItem;
import com.eu.habbo.habbohotel.modtool.ModToolSanctions; import com.eu.habbo.habbohotel.modtool.ModToolSanctions;
import com.eu.habbo.habbohotel.permissions.Permission; import com.eu.habbo.habbohotel.permissions.Permission;
@@ -49,6 +50,8 @@ public class ModToolSanctionTradeLockEvent extends MessageHandler {
habbo.getHabboStats().setAllowTrade(false); habbo.getHabboStats().setAllowTrade(false);
habbo.alert(message); habbo.alert(message);
} }
ModToolManager.bumpUserSettingCounter(userId, "tradelock_amount");
} else { } else {
this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name")))); this.client.sendResponse(new ModToolIssueHandledComposer(Emulator.getTexts().getValue("generic.user.not_found").replace("%user%", Emulator.getConfig().getValue("hotel.player.name"))));
} }
@@ -12,10 +12,13 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.sql.*; import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
public class ModToolUserInfoComposer extends MessageComposer { public class ModToolUserInfoComposer extends MessageComposer {
private static final Logger LOGGER = LoggerFactory.getLogger(ModToolUserInfoComposer.class); private static final Logger LOGGER = LoggerFactory.getLogger(ModToolUserInfoComposer.class);
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm");
private final ResultSet set; private final ResultSet set;
private final boolean hideMail; private final boolean hideMail;
@@ -29,37 +32,30 @@ public class ModToolUserInfoComposer extends MessageComposer {
protected ServerMessage composeInternal() { protected ServerMessage composeInternal() {
this.response.init(Outgoing.ModToolUserInfoComposer); this.response.init(Outgoing.ModToolUserInfoComposer);
try { try {
int totalBans = 0; int userId = this.set.getInt("user_id");
String machineId = this.set.getString("machine_id");
int now = Emulator.getIntUnixTimestamp();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); int totalBans = countBansForUser(userId);
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) AS amount FROM bans WHERE user_id = ?")) { int lastPurchaseTimestamp = fetchLastPurchaseTimestamp(userId);
statement.setInt(1, this.set.getInt("user_id")); int tradeLockExpiryTimestamp = fetchActiveTradeLockExpiry(userId, now);
try (ResultSet set = statement.executeQuery()) { int identityRelatedBanCount = countIdentityRelatedBans(userId, machineId);
if (set.next()) {
totalBans = set.getInt("amount");
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
this.response.appendInt(this.set.getInt("user_id")); this.response.appendInt(userId);
this.response.appendString(this.set.getString("username")); this.response.appendString(this.set.getString("username"));
this.response.appendString(this.set.getString("look")); this.response.appendString(this.set.getString("look"));
this.response.appendInt((Emulator.getIntUnixTimestamp() - this.set.getInt("account_created")) / 60); this.response.appendInt((now - this.set.getInt("account_created")) / 60);
this.response.appendInt((this.set.getInt("online") == 1 ? 0 : Emulator.getIntUnixTimestamp() - this.set.getInt("last_online")) / 60); this.response.appendInt((this.set.getInt("online") == 1 ? 0 : now - this.set.getInt("last_online")) / 60);
this.response.appendBoolean(this.set.getInt("online") == 1); this.response.appendBoolean(this.set.getInt("online") == 1);
this.response.appendInt(this.set.getInt("cfh_send")); this.response.appendInt(this.set.getInt("cfh_send"));
this.response.appendInt(this.set.getInt("cfh_abusive")); this.response.appendInt(this.set.getInt("cfh_abusive"));
this.response.appendInt(this.set.getInt("cfh_warnings")); this.response.appendInt(this.set.getInt("cfh_warnings"));
this.response.appendInt(totalBans); // Number of bans this.response.appendInt(totalBans); // Number of bans
this.response.appendInt(this.set.getInt("tradelock_amount")); this.response.appendInt(this.set.getInt("tradelock_amount"));
this.response.appendString(""); //Trading lock expiry timestamp this.response.appendString(formatUnixTimestamp(tradeLockExpiryTimestamp)); // Trading lock expiry timestamp
this.response.appendString(""); //Last Purchase Timestamp this.response.appendString(formatUnixTimestamp(lastPurchaseTimestamp)); // Last Purchase Timestamp
this.response.appendInt(this.set.getInt("user_id")); //Personal Identification # this.response.appendInt(userId); //Personal Identification #
this.response.appendInt(0); // Number of account bans this.response.appendInt(identityRelatedBanCount); // Number of account bans on the same machine_id
this.response.appendString(this.hideMail ? "" : this.set.getString("mail")); this.response.appendString(this.hideMail ? "" : this.set.getString("mail"));
this.response.appendString("Rank (" + this.set.getInt("rank_id") + "): " + this.set.getString("rank_name")); //user_class_txt this.response.appendString("Rank (" + this.set.getInt("rank_id") + "): " + this.set.getString("rank_name")); //user_class_txt
@@ -90,4 +86,87 @@ public class ModToolUserInfoComposer extends MessageComposer {
public ResultSet getSet() { public ResultSet getSet() {
return set; return set;
} }
private static int countBansForUser(int userId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(*) AS amount FROM bans WHERE user_id = ?")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) return set.getInt("amount");
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return 0;
}
/**
* Most recent purchase timestamp from logs_shop_purchases for this
* user. Returns 0 when the user has never bought anything (in which
* case the wire field stays empty and the client shows the empty
* placeholder).
*/
private static int fetchLastPurchaseTimestamp(int userId) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT MAX(`timestamp`) AS ts FROM logs_shop_purchases WHERE user_id = ?")) {
statement.setInt(1, userId);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) return set.getInt("ts");
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return 0;
}
/**
* Latest active trade-lock expiry from the sanctions table. Only
* locks expiring in the future are considered — past entries don't
* count. Returns 0 when no active lock exists.
*/
private static int fetchActiveTradeLockExpiry(int userId, int now) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT MAX(trade_locked_until) AS expiry FROM sanctions WHERE habbo_id = ? AND trade_locked_until > ?")) {
statement.setInt(1, userId);
statement.setInt(2, now);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) return set.getInt("expiry");
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return 0;
}
/**
* Count of OTHER user accounts that have been banned from the same
* machine_id as this user. An empty machine_id (default '') is
* ignored — never matches anything by definition. Self is excluded
* because the user's own bans are already counted under banCount.
*/
private static int countIdentityRelatedBans(int userId, String machineId) {
if (machineId == null || machineId.isEmpty()) return 0;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT COUNT(DISTINCT user_id) AS amount FROM bans WHERE machine_id = ? AND user_id != ?")) {
statement.setString(1, machineId);
statement.setInt(2, userId);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) return set.getInt("amount");
}
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
}
return 0;
}
/**
* Wire format for date fields is `yyyy-MM-dd HH:mm`. A 0 timestamp
* is rendered as an empty string so the client falls back to its
* empty-state placeholder.
*/
private static String formatUnixTimestamp(int timestamp) {
if (timestamp <= 0) return "";
return DATE_FORMAT.format(new Date(timestamp * 1000L));
}
} }