Compare commits

...

47 Commits

Author SHA1 Message Date
github-actions[bot] 0b142d184c 🆙 Bump version to 4.2.37 [skip ci] 2026-06-05 19:21:31 +00:00
DuckieTM 867c8ff857 Merge pull request #155 from duckietm/dev
🆙 Fix the Admin Catalogue stuff
2026-06-05 21:20:31 +02:00
duckietm 5094d6ce4f 🆙 Fix the Admin Catalogue stuff 2026-06-05 14:23:05 +02:00
github-actions[bot] 2c0ef9873c 🆙 Bump version to 4.2.36 [skip ci] 2026-06-04 08:44:19 +00:00
DuckieTM dadc1b8aaf Merge pull request #153 from duckietm/dev
Dev
2026-06-04 10:43:21 +02:00
duckietm 85758b53fa 🆙 Updates Mention 2026-06-04 10:43:05 +02:00
DuckieTM 2171b5f2ec Merge pull request #152 from medievalshell/feat/mentions-hotelwide-figure
feat(mentions): hotel-wide @nick delivery + sender figure + disable-m…
2026-06-04 08:50:49 +02:00
medievalshell 46306c8205 feat(mentions): hotel-wide @nick delivery + sender figure + disable-mention persistence
- resolveHabbo() falls back to a hotel-wide online lookup so a direct @nick
  mention reaches the target even when they are in a different room (was
  resolved only within the sender's room).
- HabboMention now carries the sender figure (live from the sender Habbo,
  history from a users.look JOIN); MentionReceived/MentionsList composers
  append it so the client can render the sender avatar in the notification.
- 009: add users_settings.mentions_enabled / mass_mentions_enabled columns
  so :disablementions / :disablemassmentions actually persist.
2026-06-04 01:27:45 +02:00
github-actions[bot] fadec887cd 🆙 Bump version to 4.2.35 [skip ci] 2026-06-03 14:45:16 +00:00
DuckieTM e614c1d64f Merge pull request #150 from duckietm/dev
Merge pull request #149 from duckietm/main
2026-06-03 16:44:04 +02:00
DuckieTM e7deea7d9d Merge pull request #149 from duckietm/main
sync to dev
2026-06-03 16:39:01 +02:00
github-actions[bot] 44ea3abd4e 🆙 Bump version to 4.2.34 [skip ci] 2026-06-03 14:37:38 +00:00
DuckieTM 609cd20ab2 Merge pull request #143 from simoleo89/feat/command-autocomplete-refactor
Structure commands alert output
2026-06-03 16:36:33 +02:00
github-actions[bot] 717a7f184f 🆙 Bump version to 4.2.33 [skip ci] 2026-06-03 14:23:40 +00:00
DuckieTM 2862446686 Merge pull request #148 from duckietm/dev
🆙 More updates mentions
2026-06-03 16:22:39 +02:00
duckietm e97e680006 🆙 More updates mentions 2026-06-03 16:20:02 +02:00
github-actions[bot] 7e59dca192 🆙 Bump version to 4.2.32 [skip ci] 2026-06-03 12:20:44 +00:00
DuckieTM 1109d53720 Merge pull request #147 from duckietm/dev
Dev
2026-06-03 14:19:42 +02:00
duckietm f12363a5b7 Merge branch 'dev' of https://github.com/duckietm/Arcturus-Morningstar-Extended into dev 2026-06-03 14:17:28 +02:00
duckietm 7d4ffec74e 🆙 Small Fixes mention 2026-06-03 14:17:25 +02:00
github-actions[bot] 281fede58c 🆙 Bump version to 4.2.31 [skip ci] 2026-06-03 08:56:45 +00:00
DuckieTM edf152485b Merge pull request #145 from duckietm/dev
Dev
2026-06-03 10:55:46 +02:00
DuckieTM 18a1bfbe90 Merge branch 'main' into dev 2026-06-03 10:55:37 +02:00
duckietm 7c32bbfd2d 🆙 wordfilter to set specific settings to prefix 2026-06-03 10:39:44 +02:00
DuckieTM 4eae206b64 Merge pull request #140 from simoleo89/feat/mentions-system
feat(mentions): server-side mention detection, persistence & packets
2026-06-03 09:49:45 +02:00
github-actions[bot] 155b2202c7 🆙 Bump version to 4.2.30 [skip ci] 2026-06-03 07:48:08 +00:00
DuckieTM 10c291eb9f Merge pull request #144 from duckietm/dev
Dev
2026-06-03 09:47:03 +02:00
duckietm 349a8c727e 🆙 Update SQL 2026-06-03 09:46:43 +02:00
duckietm 68f2b71d14 🆙 Updated Prefixes : Now use wordfilter / table custom_prefix_blacklist can be droped 2026-06-03 09:42:43 +02:00
duckietm 69a6c0d060 🆙 Make group forums private, so only memeber can view it 2026-06-03 07:46:59 +02:00
simoleo89 9f36d95dbc fix(commands): structure commands alert output 2026-06-02 18:34:50 +02:00
github-actions[bot] 885bdca0c4 🆙 Bump version to 4.2.29 [skip ci] 2026-06-02 16:03:45 +00:00
DuckieTM db035294a7 Merge pull request #142 from duckietm/dev
🆙 Updated Group buy
2026-06-02 18:02:42 +02:00
duckietm 3216ba1df6 🆙 Updated Group buy 2026-06-02 18:02:25 +02:00
Life c9a47b1fac Merge branch 'duckietm:main' into feat/mentions-system 2026-06-02 17:38:25 +02:00
github-actions[bot] 8d6b969d75 🆙 Bump version to 4.2.28 [skip ci] 2026-06-02 14:06:26 +00:00
DuckieTM b9723e0298 Merge pull request #141 from duckietm/dev
🆙 Security Fix
2026-06-02 16:05:11 +02:00
duckietm c4aae676b2 🆙 Security Fix
Thanks to @Bop:

There's a group bug where you can accept anyone into a group within MS. There's no packet validation for accepting members if the group is invite only.
This is crucial because if you allow users to have rights who are group members, your rooms can be trashed. AKA YOUR EVENT ROOMS
2026-06-02 16:04:47 +02:00
simoleo89 7624d3fbc3 feat(mentions): server-side delete packet + robust direct-nick resolution 2026-06-02 14:44:08 +02:00
github-actions[bot] 585f4dd3aa 🆙 Bump version to 4.2.27 [skip ci] 2026-06-01 06:28:06 +00:00
DuckieTM afa114d511 Merge pull request #139 from duckietm/dev
Dev
2026-06-01 08:27:01 +02:00
simoleo89 e9129576a9 feat(mentions): server-side detection, persistence and packets 2026-05-31 21:47:56 +02:00
DuckieTM 0aadd01493 Merge pull request #138 from simoleo89/feat/wheel-admin-add-remove
feat(wheel): add & remove fortune-wheel prizes from the editor
2026-05-31 15:45:10 +02:00
simoleo89 9d98fbf9ee feat(wheel): support adding & removing fortune-wheel prizes from the editor
The prize editor could only update existing rows; savePrize was UPDATE-only,
so the admin panel had no way to add a new slice or remove an old one.

- WheelManager.savePrize now takes a sortOrder and inserts when id <= 0
  (returning the generated id) or updates + re-enables when id > 0, so a
  previously removed prize can be brought back. sort_order is persisted to
  match the editor's display order.
- New WheelManager.disablePrizesNotIn(keptIds) soft-deletes (enabled = 0)
  any prize absent from the saved authoritative list. Non-destructive: rows
  stay in the table and loadPrizes already filters enabled = 1.
- WheelAdminSavePrizesEvent collects the saved ids and disables the rest
  before reloading.

No schema change (wheel_prizes already has enabled + sort_order) and no
packet change (id = 0 / omission express insert / delete on the existing
wire). Pairs with the Nitro-V3 client editor add/remove buttons.
2026-05-31 10:49:10 +02:00
DuckieTM b38274e134 Merge pull request #137 from medievalshell/Dev
fix(bans): persist client machine fingerprint so machine/super bans work
2026-05-31 10:37:54 +02:00
medievalshell 02ab30180c fix(chat): relay unknown chat bubble ids instead of resetting to default
getBubble() fell back to NORMAL (bubble 0) for any id not in the registered
BUBBLES map, so custom client-side chat bubbles (e.g. ids 253+) rendered as
the default bubble for everyone. Now unknown positive ids (<=1000) pass
through as a transient bubble carrying that id, so the server relays it and
clients render their own .bubble-<id> style. No need to enumerate each one.
2026-05-31 03:39:23 +02:00
medievalshell da63439d53 fix(bans): persist client machine fingerprint so machine/super bans work
The Nitro client already sends a strong machine fingerprint (Thumbmark,
"IID-<hash>") via the UniqueID packet (header 2490 -> MachineIDEvent), but
the emulator only stored it on the GameClient and never copied it onto the
Habbo's HabboInfo, so it was never written to users.machine_id. As a result
machine/super bans (which read users.machine_id) matched nobody.

- MachineIDEvent: when the fingerprint arrives and the Habbo is already
  loaded, copy it onto HabboInfo and persist (run the Habbo save).
- SecureLoginEvent: if the fingerprint arrived before login, copy it onto
  HabboInfo right before the login save.

This makes machine/super bans effective without changing the client.
2026-05-31 00:04:00 +02:00
49 changed files with 1599 additions and 320 deletions
@@ -322,13 +322,6 @@ CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
PRIMARY KEY (`key_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`word` VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_custom_prefix_blacklist_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT IGNORE INTO `custom_prefix_settings` (`key_name`, `value`) VALUES
('max_length', '15'),
('min_rank_to_buy', '1'),
@@ -0,0 +1,89 @@
CREATE TABLE IF NOT EXISTS `habbo_mentions` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`target_user_id` INT(11) NOT NULL,
`sender_user_id` INT(11) NOT NULL,
`sender_username` VARCHAR(64) NOT NULL DEFAULT '',
`room_id` INT(11) NOT NULL DEFAULT 0,
`room_name` VARCHAR(64) NOT NULL DEFAULT '',
`message` VARCHAR(255) NOT NULL DEFAULT '',
`mention_type` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '0 = direct (@nick), 1 = broadcast (@all/@friends/@room)',
`timestamp` INT(11) NOT NULL DEFAULT 0,
`read` TINYINT(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
KEY `idx_target_id` (`target_user_id`, `id`),
KEY `idx_target_unread` (`target_user_id`, `read`),
KEY `idx_target_timestamp` (`target_user_id`, `timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INSERT INTO `permission_definitions`
(`permission_key`, `max_value`, `comment`,
`rank_1`, `rank_2`, `rank_3`, `rank_4`, `rank_5`, `rank_6`, `rank_7`, `rank_8`)
VALUES
('acc_mention_everyone', 1,
'Allow sending @all / @everyone / @tutti broadcast mentions (hotel-wide).',
0, 0, 0, 0, 1, 1, 1, 1),
('acc_mention_friends', 1,
'Allow sending @friends / @amici broadcast mentions (sender''s online buddies).',
0, 0, 0, 0, 1, 1, 1, 1),
('cmd_disablementions', 1,
'Allow toggling :disablementions to stop receiving any @mention notifications.',
1, 1, 1, 1, 1, 1, 1, 1),
('cmd_disablemassmentions', 1,
'Allow toggling :disablemassmentions to stop receiving broadcast mentions (direct @nick still works).',
1, 1, 1, 1, 1, 1, 1, 1)
ON DUPLICATE KEY UPDATE
`comment` = VALUES(`comment`);
-- ----------------------------------------------------------------------------
-- 3. Emulator settings: cooldowns, caps and alias lists
--
-- Only inserted when missing - existing tuned values are preserved.
-- ----------------------------------------------------------------------------
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
('mentions.enabled', '1',
'Master switch. 1 = process @mentions, 0 = disable the feature entirely.'),
('mentions.max.targets', '50',
'Hard cap on how many users a single broadcast (@all / @friends / @room) can fan out to.'),
('mentions.cooldown.ms', '3000',
'Per-sender cooldown between any two mentions, in milliseconds.'),
('mentions.room.cooldown.ms', '15000',
'Extra per-sender cooldown for broadcast mentions (@all / @friends / @room) on top of mentions.cooldown.ms.'),
('mentions.store.limit', '50',
'Number of mentions returned in the initial RequestMentionsList response.'),
('mentions.request.cooldown.ms', '2000',
'Per-user cooldown between RequestMentionsList packets.'),
('mentions.markread.cooldown.ms', '500',
'Per-user cooldown between mark-single-as-read packets.'),
('mentions.markall.cooldown.ms', '5000',
'Per-user cooldown between mark-all-as-read packets (bulk DB update).'),
('mentions.delete.cooldown.ms', '500',
'Per-user cooldown between delete-mention packets.'),
('mentions.everyone.aliases', 'all,everyone,tutti',
'Comma-separated aliases that trigger an @everyone broadcast (requires acc_mention_everyone).'),
('mentions.friends.aliases', 'friends,amici',
'Comma-separated aliases that trigger an @friends broadcast (requires acc_mention_friends).'),
('mentions.room.aliases', 'room,stanza',
'Comma-separated aliases that trigger an @room broadcast (no permission required, room scope only).');
ALTER TABLE `wordfilter`
ADD COLUMN `prefix_only` ENUM('0','1') NOT NULL DEFAULT '0'
COMMENT 'When 1, this word only applies to custom prefixes, not to chat/motto/guild.' AFTER `mute`;
-- ----------------------------------------------------------------------------
-- 5. Per-user mention preferences (:disablementions / :disablemassmentions)
--
-- Read by HabboStats (default '1' = enabled), toggled by the commands.
-- Without these columns the toggle commands cannot persist.
-- ----------------------------------------------------------------------------
ALTER TABLE `users_settings`
ADD COLUMN IF NOT EXISTS `mentions_enabled` ENUM('0','1') NOT NULL DEFAULT '1'
COMMENT 'Receive @nick mention notifications.',
ADD COLUMN IF NOT EXISTS `mass_mentions_enabled` ENUM('0','1') NOT NULL DEFAULT '1'
COMMENT 'Receive broadcast (@all / @friends / @room) mentions.';
@@ -63,15 +63,6 @@ CREATE TABLE IF NOT EXISTS `custom_prefix_settings` (
PRIMARY KEY (`key_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ------------------------------------------------------------
-- 5. Blacklist table
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `custom_prefix_blacklist` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`word` VARCHAR(100) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_word` (`word`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ============================================================
-- Schema upgrades for existing installations
@@ -296,14 +287,6 @@ INSERT IGNORE INTO `custom_prefixes_catalog`
(2, 'Legend', 'Legend', '#8B5CF6', '', 'discord-neon', '', 15, 0, 1, 2),
(3, 'Staff Pick', 'Staff', '#3B82F6', '*', 'cartoon', '', 20, 0, 1, 3);
-- ============================================================
-- Example blacklist entries
-- ============================================================
INSERT IGNORE INTO `custom_prefix_blacklist` (`word`) VALUES
('admin'),
('staff'),
('mod'),
('owner');
-- ============================================================
-- Notes
+1 -1
View File
@@ -6,7 +6,7 @@
<groupId>com.eu.habbo</groupId>
<artifactId>Habbo</artifactId>
<version>4.2.26</version>
<version>4.2.37</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -8,6 +8,7 @@ import com.eu.habbo.habbohotel.campaign.calendar.CalendarManager;
import com.eu.habbo.habbohotel.catalog.CatalogManager;
import com.eu.habbo.habbohotel.wheel.WheelManager;
import com.eu.habbo.habbohotel.soundboard.SoundboardManager;
import com.eu.habbo.habbohotel.mentions.MentionManager;
import com.eu.habbo.habbohotel.commands.CommandHandler;
import com.eu.habbo.habbohotel.crafting.CraftingManager;
import com.eu.habbo.habbohotel.guides.GuideManager;
@@ -68,6 +69,7 @@ public class GameEnvironment {
private InfostandBackgroundManager infostandBackgroundManager;
private WheelManager wheelManager;
private SoundboardManager soundboardManager;
private MentionManager mentionManager;
public void load() throws Exception {
LOGGER.info("GameEnvironment -> Loading...");
@@ -99,6 +101,7 @@ public class GameEnvironment {
this.infostandBackgroundManager = new InfostandBackgroundManager();
this.wheelManager = new WheelManager();
this.soundboardManager = new SoundboardManager();
this.mentionManager = new MentionManager();
this.roomManager.loadPublicRooms();
this.navigatorManager.loadNavigator();
@@ -202,6 +205,10 @@ public class GameEnvironment {
return this.petManager;
}
public MentionManager getMentionManager() {
return this.mentionManager;
}
public AchievementManager getAchievementManager() {
return this.achievementManager;
}
@@ -191,6 +191,8 @@ public class CommandHandler {
addCommand(new CreditsCommand());
addCommand(new DanceCommand());
addCommand(new DiagonalCommand());
addCommand(new DisableMassMentionsCommand());
addCommand(new DisableMentionsCommand());
addCommand(new DisconnectCommand());
addCommand(new EjectAllCommand());
addCommand(new EmptyInventoryCommand());
@@ -301,7 +303,6 @@ public class CommandHandler {
addCommand(new GivePrefixCommand());
addCommand(new ListPrefixesCommand());
addCommand(new RemovePrefixCommand());
addCommand(new PrefixBlacklistCommand());
addCommand(new WiredCommand());
addCommand(new TestCommand());
}
@@ -17,7 +17,22 @@ public class CommandsCommand extends Command {
message.append("(").append(commands.size()).append("):\r\n");
for (Command c : commands) {
message.append(Emulator.getTexts().getValue("commands.description." + c.permission, "commands.description." + c.permission)).append("\r");
String textKey = "commands.description." + c.permission;
String commandText = Emulator.getTexts().getValue(textKey, "");
String commandLine = ":" + c.keys[0];
String description = "";
if (commandText.startsWith(":")) {
commandLine = commandText;
} else if (!commandText.isEmpty() && !commandText.equals(textKey)) {
description = commandText;
}
message.append(commandLine).append("\r");
if (!description.isEmpty()) {
message.append(description).append("\r");
}
}
gameClient.getHabbo().alert(new String[]{message.toString()});
@@ -0,0 +1,25 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.users.Habbo;
public class DisableMassMentionsCommand extends Command {
public DisableMassMentionsCommand() {
super("cmd_disablemassmentions", new String[]{"disablemassmentions", "togglemassmentions"});
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (gameClient == null) return true;
Habbo habbo = gameClient.getHabbo();
if (habbo == null || habbo.getHabboStats() == null) return true;
boolean newState = !habbo.getHabboStats().massMentionsEnabled();
habbo.getHabboStats().setMassMentionsEnabled(newState);
habbo.whisper(newState
? "Broadcast mentions (@all / @friends / @room) are now ENABLED for you."
: "Broadcast mentions (@all / @friends / @room) are now DISABLED for you. Direct @nick mentions still work.");
return true;
}
}
@@ -0,0 +1,25 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.users.Habbo;
public class DisableMentionsCommand extends Command {
public DisableMentionsCommand() {
super("cmd_disablementions", new String[]{"disablementions", "togglementions"});
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (gameClient == null) return true;
Habbo habbo = gameClient.getHabbo();
if (habbo == null || habbo.getHabboStats() == null) return true;
boolean newState = !habbo.getHabboStats().mentionsEnabled();
habbo.getHabboStats().setMentionsEnabled(newState);
habbo.whisper(newState
? "@mention notifications are now ENABLED for you."
: "@mention notifications are now DISABLED for you. You will not receive direct or broadcast mentions.");
return true;
}
}
@@ -4,6 +4,7 @@ import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.modtool.WordFilter;
import com.eu.habbo.habbohotel.modtool.WordFilterWord;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -21,30 +22,44 @@ public class FilterWordCommand extends Command {
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (params.length < 2) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.missing_word"));
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.missing_word"), RoomChatMessageBubbles.ALERT);
return true;
}
String word = params[1];
// Optional trailing "prefix" keyword marks the word as prefix-only (blocks
// custom prefixes but not chat/motto/guild). Usage:
// :filterword <word> -> everywhere, default replacement
// :filterword <word> <replacement> -> everywhere
// :filterword <word> prefix -> prefix-only, default replacement
// :filterword <word> <replacement> prefix -> prefix-only
boolean prefixOnly = false;
String replacement = WordFilter.DEFAULT_REPLACEMENT;
if (params.length == 3) {
replacement = params[2];
if (params.length >= 3) {
if (params[params.length - 1].equalsIgnoreCase("prefix")) {
prefixOnly = true;
if (params.length >= 4) replacement = params[2];
} else {
replacement = params[2];
}
}
WordFilterWord wordFilterWord = new WordFilterWord(word, replacement);
WordFilterWord wordFilterWord = new WordFilterWord(word, replacement, prefixOnly);
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wordfilter (`key`, `replacement`) VALUES (?, ?)")) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO wordfilter (`key`, `replacement`, `prefix_only`) VALUES (?, ?, ?)")) {
statement.setString(1, word);
statement.setString(2, replacement);
statement.setString(3, prefixOnly ? "1" : "0");
statement.execute();
} catch (SQLException e) {
LOGGER.error("Caught SQL exception", e);
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.error"));
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_filterword.error"), RoomChatMessageBubbles.ALERT);
return true;
}
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_filterword.added").replace("%word%", word).replace("%replacement%", replacement));
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.succes.cmd_filterword.added").replace("%word%", word).replace("%replacement%", replacement) + (prefixOnly ? " [prefix-only]" : ""), RoomChatMessageBubbles.ALERT);
Emulator.getGameEnvironment().getWordFilter().addWord(wordFilterWord);
return true;
@@ -1,98 +0,0 @@
package com.eu.habbo.habbohotel.commands;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.gameclients.GameClient;
import com.eu.habbo.habbohotel.rooms.RoomChatMessageBubbles;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class PrefixBlacklistCommand extends Command {
private static final Logger LOGGER = LoggerFactory.getLogger(PrefixBlacklistCommand.class);
public PrefixBlacklistCommand() {
super("cmd_prefix_blacklist", Emulator.getTexts().getValue("commands.keys.cmd_prefix_blacklist").split(";"));
}
@Override
public boolean handle(GameClient gameClient, String[] params) throws Exception {
if (params.length < 2) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
return true;
}
String action = params[1].toLowerCase();
if (action.equals("list")) {
StringBuilder sb = new StringBuilder();
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.header")).append("\r");
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT word FROM custom_prefix_blacklist ORDER BY word")) {
try (ResultSet set = statement.executeQuery()) {
int count = 0;
while (set.next()) {
sb.append("- ").append(set.getString("word")).append("\r");
count++;
}
if (count == 0) {
sb.append(Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.empty"));
}
}
} catch (SQLException e) {
LOGGER.error("Error listing prefix blacklist", e);
}
gameClient.getHabbo().whisper(sb.toString(), RoomChatMessageBubbles.ALERT);
return true;
}
if (params.length < 3) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
return true;
}
String word = params[2].toLowerCase().trim();
if (word.isEmpty()) {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.empty_word"), RoomChatMessageBubbles.ALERT);
return true;
}
if (action.equals("add")) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("INSERT INTO custom_prefix_blacklist (word) VALUES (?)")) {
statement.setString(1, word);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Error adding prefix blacklist word", e);
}
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.added").replace("%word%", word),
RoomChatMessageBubbles.ALERT
);
} else if (action.equals("remove")) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("DELETE FROM custom_prefix_blacklist WHERE word = ?")) {
statement.setString(1, word);
statement.execute();
} catch (SQLException e) {
LOGGER.error("Error removing prefix blacklist word", e);
}
gameClient.getHabbo().whisper(
Emulator.getTexts().getValue("commands.succes.cmd_prefix_blacklist.removed").replace("%word%", word),
RoomChatMessageBubbles.ALERT
);
} else {
gameClient.getHabbo().whisper(Emulator.getTexts().getValue("commands.error.cmd_prefix_blacklist.usage"), RoomChatMessageBubbles.ALERT);
}
return true;
}
}
@@ -252,6 +252,25 @@ public class Guild implements Runnable {
return this.readForum;
}
public boolean canHabboReadForum(int habboId, GuildMember member, boolean staff) {
if (staff || this.getOwnerId() == habboId) {
return true;
}
switch (this.readForum) {
case EVERYONE:
return true;
case MEMBERS:
return member != null && member.getRank().type <= GuildRank.MEMBER.type;
case ADMINS:
return member != null && member.getRank().type < GuildRank.MEMBER.type;
case OWNER:
return false;
default:
return true;
}
}
public void setReadForum(SettingsState readForum) {
this.readForum = readForum;
}
@@ -0,0 +1,106 @@
package com.eu.habbo.habbohotel.mentions;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.users.Habbo;
import java.sql.ResultSet;
import java.sql.SQLException;
public class HabboMention {
public static final int TYPE_DIRECT = 0;
public static final int TYPE_ROOM = 1;
private final int id;
private final int targetUserId;
private final int senderUserId;
private final String senderUsername;
private final int roomId;
private final String roomName;
private final String message;
private final int mentionType;
private final int timestamp;
private final boolean read;
private final String senderFigure;
public HabboMention(ResultSet set) throws SQLException {
this.id = set.getInt("id");
this.targetUserId = set.getInt("target_user_id");
this.senderUserId = set.getInt("sender_user_id");
this.senderUsername = set.getString("sender_username");
this.roomId = set.getInt("room_id");
this.roomName = set.getString("room_name");
this.message = set.getString("message");
this.mentionType = set.getInt("mention_type");
this.timestamp = set.getInt("timestamp");
this.read = set.getInt("read") == 1;
this.senderFigure = hasSenderFigure(set) ? set.getString("sender_figure") : "";
}
private static boolean hasSenderFigure(ResultSet set) {
try {
set.findColumn("sender_figure");
return true;
} catch (SQLException e) {
return false;
}
}
public HabboMention(int targetUserId, int id, Habbo sender, Room room, String roomName, String message, int mentionType, int timestamp) {
this.id = id;
this.targetUserId = targetUserId;
this.senderUserId = sender.getHabboInfo().getId();
this.senderUsername = sender.getHabboInfo().getUsername();
this.roomId = room.getId();
this.roomName = roomName;
this.message = message;
this.mentionType = mentionType;
this.timestamp = timestamp;
this.read = false;
this.senderFigure = sender.getHabboInfo().getLook();
}
public int getId() {
return this.id;
}
public int getTargetUserId() {
return this.targetUserId;
}
public int getSenderUserId() {
return this.senderUserId;
}
public String getSenderUsername() {
return this.senderUsername;
}
public int getRoomId() {
return this.roomId;
}
public String getRoomName() {
return this.roomName;
}
public String getMessage() {
return this.message;
}
public int getMentionType() {
return this.mentionType;
}
public int getTimestamp() {
return this.timestamp;
}
public boolean isRead() {
return this.read;
}
public String getSenderFigure() {
return this.senderFigure == null ? "" : this.senderFigure;
}
}
@@ -0,0 +1,463 @@
package com.eu.habbo.habbohotel.mentions;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.messenger.MessengerBuddy;
import com.eu.habbo.habbohotel.rooms.Room;
import com.eu.habbo.habbohotel.rooms.RoomChatType;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.HabboManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.sql.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class MentionManager {
private static final Logger LOGGER = LoggerFactory.getLogger(MentionManager.class);
private static final int ROOM_NAME_MAX_LENGTH = 64;
private static final int MESSAGE_MAX_LENGTH = 255;
private final ConcurrentHashMap<Integer, Long> cooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> roomBroadcastCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> requestListCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> markReadCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> markAllCooldowns = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Integer, Long> deleteCooldowns = new ConcurrentHashMap<>();
private volatile long lastPrune = System.currentTimeMillis();
private static final long PRUNE_INTERVAL_MS = 5 * 60_000L;
public boolean isEnabled() {
return Emulator.getConfig().getInt("mentions.enabled", 1) == 1;
}
public enum BroadcastScope {
NONE,
ROOM,
FRIENDS,
EVERYONE
}
public static final String PERMISSION_EVERYONE = "acc_mention_everyone";
public static final String PERMISSION_FRIENDS = "acc_mention_friends";
private Set<String> parseAliases(String configKey, String defaultValue) {
Set<String> aliases = new HashSet<>();
String raw = Emulator.getConfig().getValue(configKey, defaultValue);
for (String alias : raw.split(",")) {
String trimmed = alias.trim().toLowerCase();
if (!trimmed.isEmpty()) {
aliases.add(trimmed);
}
}
return aliases;
}
private Set<String> roomAliases() {
return parseAliases("mentions.room.aliases", "room,stanza");
}
private Set<String> friendsAliases() {
return parseAliases("mentions.friends.aliases", "friends,amici");
}
private Set<String> everyoneAliases() {
return parseAliases("mentions.everyone.aliases", "all,everyone,tutti");
}
private BroadcastScope classifyAlias(String alias,
Set<String> everyone,
Set<String> friends,
Set<String> room) {
if (alias.isEmpty()) return BroadcastScope.NONE;
if (everyone.contains(alias)) return BroadcastScope.EVERYONE;
if (friends.contains(alias)) return BroadcastScope.FRIENDS;
if (room.contains(alias)) return BroadcastScope.ROOM;
return BroadcastScope.NONE;
}
public void process(Habbo sender, Room room, String message, RoomChatType type) {
try {
if (!this.isEnabled()) {
return;
}
if (sender == null || room == null || message == null) {
return;
}
if (message.isEmpty() || message.indexOf('@') < 0) {
return;
}
int senderId = sender.getHabboInfo().getId();
long now = System.currentTimeMillis();
long cooldownMs = Emulator.getConfig().getInt("mentions.cooldown.ms", 3000);
Long last = this.cooldowns.get(senderId);
if (last != null && (now - last) < cooldownMs) {
return;
}
Set<String> roomAliases = this.roomAliases();
Set<String> friendsAliases = this.friendsAliases();
Set<String> everyoneAliases = this.everyoneAliases();
BroadcastScope broadcastScope = BroadcastScope.NONE;
LinkedHashSet<String> directTokens = new LinkedHashSet<>();
for (String token : message.split("\\s+")) {
if (token.length() < 2 || token.charAt(0) != '@') {
continue;
}
String raw = token.substring(1);
String aliasCandidate = trimTrailingPunctuation(raw).toLowerCase();
BroadcastScope scope = this.classifyAlias(aliasCandidate, everyoneAliases, friendsAliases, roomAliases);
if (scope != BroadcastScope.NONE) {
if (scope.ordinal() > broadcastScope.ordinal()) {
broadcastScope = scope;
}
} else if (!raw.isEmpty()) {
directTokens.add(raw);
}
}
if (broadcastScope == BroadcastScope.EVERYONE && !sender.hasPermission(PERMISSION_EVERYONE)) {
broadcastScope = BroadcastScope.NONE;
} else if (broadcastScope == BroadcastScope.FRIENDS && !sender.hasPermission(PERMISSION_FRIENDS)) {
broadcastScope = BroadcastScope.NONE;
}
if (broadcastScope == BroadcastScope.NONE && directTokens.isEmpty()) {
return;
}
if (broadcastScope != BroadcastScope.NONE) {
long roomCooldownMs = Emulator.getConfig().getInt("mentions.room.cooldown.ms", 15000);
Long lastRoom = this.roomBroadcastCooldowns.get(senderId);
if (lastRoom != null && (now - lastRoom) < roomCooldownMs) {
return;
}
}
int maxTargets = Emulator.getConfig().getInt("mentions.max.targets", 50);
if (maxTargets <= 0) maxTargets = 1;
int maxDirectTokens = Math.min(directTokens.size(), maxTargets);
List<Habbo> targets = new ArrayList<>();
Set<Integer> seen = new HashSet<>();
switch (broadcastScope) {
case EVERYONE:
this.collectEveryoneTargets(senderId, targets, seen, maxTargets);
break;
case FRIENDS:
this.collectFriendsTargets(sender, senderId, targets, seen, maxTargets);
break;
case ROOM:
this.collectRoomTargets(room, senderId, targets, seen, maxTargets, true);
break;
case NONE:
default:
int processed = 0;
for (String token : directTokens) {
if (processed++ >= maxDirectTokens) break;
Habbo habbo = this.resolveHabbo(room, token);
if (habbo == null || habbo.getHabboInfo().getId() == senderId) {
continue;
}
if (!acceptsMention(habbo, false)) {
continue;
}
if (seen.add(habbo.getHabboInfo().getId())) {
targets.add(habbo);
}
if (targets.size() >= maxTargets) {
break;
}
}
break;
}
if (targets.isEmpty()) {
return;
}
this.cooldowns.put(senderId, now);
if (broadcastScope != BroadcastScope.NONE) this.roomBroadcastCooldowns.put(senderId, now);
this.pruneCooldownsIfDue(now);
int mentionType = (broadcastScope != BroadcastScope.NONE) ? HabboMention.TYPE_ROOM : HabboMention.TYPE_DIRECT;
int timestamp = Emulator.getIntUnixTimestamp();
String roomName = truncate(room.getName(), ROOM_NAME_MAX_LENGTH);
String storedMessage = truncate(message, MESSAGE_MAX_LENGTH);
for (Habbo target : targets) {
this.store(target, sender, room, storedMessage, mentionType, timestamp, roomName);
}
} catch (Exception e) {
LOGGER.error("Failed to process mentions.", e);
}
}
private void collectRoomTargets(Room room, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets, boolean isBroadcast) {
for (Habbo habbo : room.getHabbos()) {
if (habbo == null || habbo.getHabboInfo().getId() == senderId) continue;
if (!acceptsMention(habbo, isBroadcast)) continue;
if (seen.add(habbo.getHabboInfo().getId())) targets.add(habbo);
if (targets.size() >= maxTargets) break;
}
}
private void collectFriendsTargets(Habbo sender, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
if (sender.getMessenger() == null) return;
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
for (MessengerBuddy buddy : sender.getMessenger().getFriends().values()) {
if (buddy == null) continue;
int buddyId = buddy.getId();
if (buddyId == senderId) continue;
Habbo online = habboManager.getHabbo(buddyId);
if (online == null) continue;
if (!acceptsMention(online, true)) continue;
if (seen.add(buddyId)) targets.add(online);
if (targets.size() >= maxTargets) break;
}
}
private void collectEveryoneTargets(int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
for (Habbo habbo : Emulator.getGameEnvironment().getHabboManager().getOnlineHabbos().values()) {
if (habbo == null || habbo.getHabboInfo().getId() == senderId) continue;
if (!acceptsMention(habbo, true)) continue;
if (seen.add(habbo.getHabboInfo().getId())) targets.add(habbo);
if (targets.size() >= maxTargets) break;
}
}
private boolean acceptsMention(Habbo recipient, boolean isBroadcast) {
if (recipient == null) return false;
if (recipient.getClient() == null) return false;
if (recipient.getHabboStats() == null) return false;
if (!recipient.getHabboStats().mentionsEnabled()) return false;
if (isBroadcast && !recipient.getHabboStats().massMentionsEnabled()) return false;
return true;
}
private void store(Habbo target, Habbo sender, Room room, String message, int mentionType, int timestamp, String roomName) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"INSERT INTO habbo_mentions (target_user_id, sender_user_id, sender_username, room_id, room_name, message, mention_type, timestamp, `read`) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setInt(1, target.getHabboInfo().getId());
statement.setInt(2, sender.getHabboInfo().getId());
statement.setString(3, sender.getHabboInfo().getUsername());
statement.setInt(4, room.getId());
statement.setString(5, roomName);
statement.setString(6, message);
statement.setInt(7, mentionType);
statement.setInt(8, timestamp);
statement.executeUpdate();
int generatedId = 0;
try (ResultSet keys = statement.getGeneratedKeys()) {
if (keys.next()) {
generatedId = keys.getInt(1);
}
}
if (generatedId <= 0) {
return;
}
HabboMention mention = new HabboMention(target.getHabboInfo().getId(), generatedId, sender, room, roomName, message, mentionType, timestamp);
if (target.getClient() != null) {
target.getClient().sendResponse(new com.eu.habbo.messages.outgoing.mentions.MentionReceivedComposer(mention));
}
} catch (SQLException e) {
LOGGER.error("Failed to store mention.", e);
}
}
public List<HabboMention> getMentions(int userId, int limit) {
List<HabboMention> mentions = new ArrayList<>();
if (limit <= 0) limit = 50;
if (limit > 200) limit = 200;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"SELECT habbo_mentions.*, users.look AS sender_figure FROM habbo_mentions LEFT JOIN users ON users.id = habbo_mentions.sender_user_id WHERE target_user_id = ? ORDER BY id DESC LIMIT ?")) {
statement.setInt(1, userId);
statement.setInt(2, limit);
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
mentions.add(new HabboMention(set));
}
}
} catch (SQLException e) {
LOGGER.error("Failed to load mentions.", e);
}
return mentions;
}
public void markRead(int userId, int mode, int mentionId) {
if (mode != 0 && mode != 1) return;
if (mode == 1 && mentionId <= 0) return;
String query = mode == 1
? "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ? AND id = ? AND `read` = 0"
: "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ? AND `read` = 0";
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(query)) {
statement.setInt(1, userId);
if (mode == 1) {
statement.setInt(2, mentionId);
}
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to mark mentions as read.", e);
}
}
public void delete(int userId, int mentionId) {
if (mentionId <= 0) return;
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"DELETE FROM habbo_mentions WHERE target_user_id = ? AND id = ?")) {
statement.setInt(1, userId);
statement.setInt(2, mentionId);
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to delete mention.", e);
}
}
public boolean tryAcquireRequestList(int userId) {
long cooldownMs = Emulator.getConfig().getInt("mentions.request.cooldown.ms", 2000);
return tryAcquire(this.requestListCooldowns, userId, cooldownMs);
}
public boolean tryAcquireMarkRead(int userId, int mode) {
long cooldownMs;
ConcurrentHashMap<Integer, Long> bucket;
if (mode == 1) {
cooldownMs = Emulator.getConfig().getInt("mentions.markread.cooldown.ms", 500);
bucket = this.markReadCooldowns;
} else {
cooldownMs = Emulator.getConfig().getInt("mentions.markall.cooldown.ms", 5000);
bucket = this.markAllCooldowns;
}
return tryAcquire(bucket, userId, cooldownMs);
}
public boolean tryAcquireDelete(int userId) {
long cooldownMs = Emulator.getConfig().getInt("mentions.delete.cooldown.ms", 500);
return tryAcquire(this.deleteCooldowns, userId, cooldownMs);
}
private boolean tryAcquire(ConcurrentHashMap<Integer, Long> bucket, int userId, long cooldownMs) {
long now = System.currentTimeMillis();
Long last = bucket.get(userId);
if (last != null && (now - last) < cooldownMs) {
return false;
}
bucket.put(userId, now);
this.pruneCooldownsIfDue(now);
return true;
}
private void pruneCooldownsIfDue(long now) {
if (now - this.lastPrune < PRUNE_INTERVAL_MS) return;
this.lastPrune = now;
long mentionWindow = Emulator.getConfig().getInt("mentions.cooldown.ms", 3000);
long roomWindow = Emulator.getConfig().getInt("mentions.room.cooldown.ms", 15000);
long requestWindow = Emulator.getConfig().getInt("mentions.request.cooldown.ms", 2000);
long markReadWindow = Emulator.getConfig().getInt("mentions.markread.cooldown.ms", 500);
long markAllWindow = Emulator.getConfig().getInt("mentions.markall.cooldown.ms", 5000);
long deleteWindow = Emulator.getConfig().getInt("mentions.delete.cooldown.ms", 500);
prune(this.cooldowns, now, mentionWindow);
prune(this.roomBroadcastCooldowns, now, roomWindow);
prune(this.requestListCooldowns, now, requestWindow);
prune(this.markReadCooldowns, now, markReadWindow);
prune(this.markAllCooldowns, now, markAllWindow);
prune(this.deleteCooldowns, now, deleteWindow);
}
private static void prune(ConcurrentHashMap<Integer, Long> bucket, long now, long windowMs) {
Iterator<Map.Entry<Integer, Long>> it = bucket.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, Long> entry = it.next();
Long value = entry.getValue();
if (value == null || (now - value) >= windowMs) {
it.remove();
}
}
}
private static final String TRAILING_PUNCTUATION = ".,!?;:)]}\"'";
private static String trimTrailingPunctuation(String value) {
int end = value.length();
while (end > 0 && TRAILING_PUNCTUATION.indexOf(value.charAt(end - 1)) >= 0) {
end--;
}
return value.substring(0, end);
}
private static String truncate(String value, int max) {
if (value == null) return "";
if (value.length() <= max) return value;
return value.substring(0, max);
}
private boolean isBotOrPetName(Room room, String token) {
if (room == null || token == null || token.isEmpty()) return false;
List<com.eu.habbo.habbohotel.bots.Bot> bots = room.getBots(token);
if (bots != null && !bots.isEmpty()) return true;
if (room.getUnitManager() != null && room.getUnitManager().getPets() != null) {
for (com.eu.habbo.habbohotel.pets.Pet pet : room.getUnitManager().getPets()) {
if (pet != null && pet.getName() != null && pet.getName().equalsIgnoreCase(token)) {
return true;
}
}
}
return false;
}
private Habbo resolveHabbo(Room room, String rawToken) {
if (isBotOrPetName(room, rawToken)) {
return null;
}
String trimmedForBotCheck = trimTrailingPunctuation(rawToken);
if (!trimmedForBotCheck.equals(rawToken) && isBotOrPetName(room, trimmedForBotCheck)) {
return null;
}
Habbo habbo = room.getHabbo(rawToken);
if (habbo != null) {
return habbo;
}
HabboManager habboManager = Emulator.getGameEnvironment().getHabboManager();
habbo = habboManager.getHabbo(rawToken);
if (habbo != null) {
return habbo;
}
String trimmed = trimTrailingPunctuation(rawToken);
if (!trimmed.isEmpty() && !trimmed.equals(rawToken)) {
habbo = room.getHabbo(trimmed);
if (habbo != null) {
return habbo;
}
return habboManager.getHabbo(trimmed);
}
return null;
}
}
@@ -23,7 +23,6 @@ public class WordFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(WordFilter.class);
private static final Pattern DIACRITICS_AND_FRIENDS = Pattern.compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
//Configuration. Loaded from database & updated accordingly.
public static boolean ENABLED_FRIENDCHAT = true;
public static String DEFAULT_REPLACEMENT = "bobba";
protected THashSet<WordFilterWord> autoReportWords = new THashSet<>();
@@ -63,10 +62,12 @@ public class WordFilter {
continue;
}
if (word.autoReport)
this.autoReportWords.add(word);
else if (word.hideMessage)
this.hideMessageWords.add(word);
if (!word.prefixOnly) {
if (word.autoReport)
this.autoReportWords.add(word);
else if (word.hideMessage)
this.hideMessageWords.add(word);
}
this.words.add(word);
}
@@ -146,6 +147,8 @@ public class WordFilter {
while (iterator.hasNext()) {
WordFilterWord word = (WordFilterWord) iterator.next();
if (word.prefixOnly) continue;
if (StringUtils.containsIgnoreCase(filteredMessage, word.key)) {
if (habbo != null) {
if (Emulator.getPluginManager().fireEvent(new UserTriggerWordFilterEvent(habbo, word)).isCancelled())
@@ -179,6 +182,8 @@ public class WordFilter {
while (iterator.hasNext()) {
WordFilterWord word = (WordFilterWord) iterator.next();
if (word.prefixOnly) continue;
if (StringUtils.containsIgnoreCase(message, word.key)) {
if (habbo != null) {
if (Emulator.getPluginManager().fireEvent(new UserTriggerWordFilterEvent(habbo, word)).isCancelled())
@@ -9,6 +9,7 @@ public class WordFilterWord {
public final boolean hideMessage;
public final boolean autoReport;
public final int muteTime;
public final boolean prefixOnly;
public WordFilterWord(ResultSet set) throws SQLException {
this.key = set.getString("key");
@@ -16,13 +17,27 @@ public class WordFilterWord {
this.hideMessage = set.getInt("hide") == 1;
this.autoReport = set.getInt("report") == 1;
this.muteTime = set.getInt("mute");
this.prefixOnly = readBooleanColumn(set, "prefix_only");
}
public WordFilterWord(String key, String replacement) {
this(key, replacement, false);
}
public WordFilterWord(String key, String replacement, boolean prefixOnly) {
this.key = key;
this.replacement = replacement;
this.hideMessage = false;
this.autoReport = false;
this.muteTime = 0;
this.prefixOnly = prefixOnly;
}
private static boolean readBooleanColumn(ResultSet set, String column) {
try {
return set.getInt(column) == 1;
} catch (SQLException e) {
return false;
}
}
}
@@ -134,7 +134,18 @@ public class RoomChatMessageBubbles {
}
public static RoomChatMessageBubbles getBubble(int id) {
return BUBBLES.getOrDefault(id, NORMAL);
RoomChatMessageBubbles bubble = BUBBLES.get(id);
if (bubble != null) return bubble;
// Custom chat bubbles (client-side only, e.g. ids 253+) are not registered
// above. Instead of falling back to NORMAL (which made them render as the
// default bubble), pass the id through so the server relays it as-is and
// the client renders its own .bubble-<id> style. Capped to avoid abuse.
if (id > 0 && id <= 1000) {
return new RoomChatMessageBubbles(id, "CUSTOM_" + id, "", true, false);
}
return NORMAL;
}
private static void registerBubble(RoomChatMessageBubbles bubble) {
@@ -94,6 +94,8 @@ public class HabboStats implements Runnable {
public boolean hasGottenDefaultSavedSearches;
private HabboInfo habboInfo;
private boolean allowTrade;
private boolean mentionsEnabled;
private boolean massMentionsEnabled;
private int clubExpireTimestamp;
private int muteEndTime;
public int maxFriends;
@@ -131,6 +133,8 @@ public class HabboStats implements Runnable {
this.guilds = new ArrayList<>();
this.tags = set.getString("tags").split(";");
this.allowTrade = set.getString("can_trade").equals("1");
this.mentionsEnabled = "1".equals(safeColumnString(set, "mentions_enabled", "1"));
this.massMentionsEnabled = "1".equals(safeColumnString(set, "mass_mentions_enabled", "1"));
this.votedRooms = new TIntArrayStack();
this.clubExpireTimestamp = set.getInt("club_expire_timestamp");
this.loginStreak = set.getInt("login_streak");
@@ -749,13 +753,6 @@ public class HabboStats implements Runnable {
return 0;
}
/**
* Ignore an user.
*
* @param gameClient The client to which this HabboStats instance belongs.
* @param userId The user to ignore.
* @return true if successfully ignored, false otherwise.
*/
public boolean ignoreUser(GameClient gameClient, int userId) {
final Habbo target = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
@@ -805,6 +802,44 @@ public class HabboStats implements Runnable {
else return this.allowTrade;
}
public boolean mentionsEnabled() {
return this.mentionsEnabled;
}
public boolean massMentionsEnabled() {
return this.massMentionsEnabled;
}
public void setMentionsEnabled(boolean enabled) {
this.mentionsEnabled = enabled;
persistFlag("mentions_enabled", enabled);
}
public void setMassMentionsEnabled(boolean enabled) {
this.massMentionsEnabled = enabled;
persistFlag("mass_mentions_enabled", enabled);
}
private void persistFlag(String column, boolean enabled) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("UPDATE users_settings SET `" + column + "` = ? WHERE user_id = ? LIMIT 1")) {
statement.setString(1, enabled ? "1" : "0");
statement.setInt(2, this.habboInfo.getId());
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to persist users_settings.{} for user {}", column, this.habboInfo.getId(), e);
}
}
private static String safeColumnString(ResultSet set, String column, String defaultValue) {
try {
String value = set.getString(column);
return value == null ? defaultValue : value;
} catch (SQLException e) {
return defaultValue;
}
}
public void setAllowTrade(boolean allowTrade) {
this.allowTrade = allowTrade;
}
@@ -12,9 +12,11 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
@@ -330,26 +332,88 @@ public class WheelManager {
return true;
}
public void savePrize(int id, String type, String value, int amount, int pointsType, int weight, String label) {
/**
* Persists a single prize. An {@code id <= 0} inserts a brand-new prize and
* returns its generated id; a positive id updates the existing row (and
* re-enables it, so a previously soft-deleted prize can be brought back).
* {@code sortOrder} reflects the prize's position in the editor so the
* wheel layout matches what the admin sees. Returns the effective row id,
* or {@code 0} if the write failed.
*/
public int savePrize(int id, String type, String value, int amount, int pointsType, int weight, String label, int sortOrder) {
String safeType = (type != null && VALID_PRIZE_TYPES.contains(type)) ? type : "nothing";
String safeValue = truncate(value, MAX_STRING_LEN);
String safeLabel = truncate(label, MAX_STRING_LEN);
int safeAmount = clamp(amount, 0, MAX_PRIZE_AMOUNT);
int safeWeight = clamp(weight, 0, MAX_WEIGHT);
int safeSort = clamp(sortOrder, 0, MAX_PRIZES_PER_SAVE);
if (id > 0) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE wheel_prizes SET type = ?, value = ?, amount = ?, points_type = ?, weight = ?, label = ?, sort_order = ?, enabled = 1 WHERE id = ?")) {
statement.setString(1, safeType);
statement.setString(2, safeValue);
statement.setInt(3, safeAmount);
statement.setInt(4, pointsType);
statement.setInt(5, safeWeight);
statement.setString(6, safeLabel);
statement.setInt(7, safeSort);
statement.setInt(8, id);
statement.executeUpdate();
return id;
} catch (SQLException e) {
LOGGER.error("Failed to save wheel prize {}", id, e);
return 0;
}
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(
"UPDATE wheel_prizes SET type = ?, value = ?, amount = ?, points_type = ?, weight = ?, label = ? WHERE id = ?")) {
"INSERT INTO wheel_prizes (type, value, amount, points_type, weight, label, enabled, sort_order) VALUES (?, ?, ?, ?, ?, ?, 1, ?)",
Statement.RETURN_GENERATED_KEYS)) {
statement.setString(1, safeType);
statement.setString(2, safeValue);
statement.setInt(3, safeAmount);
statement.setInt(4, pointsType);
statement.setInt(5, safeWeight);
statement.setString(6, safeLabel);
statement.setInt(7, id);
statement.setInt(7, safeSort);
statement.executeUpdate();
try (ResultSet keys = statement.getGeneratedKeys()) {
if (keys.next()) return keys.getInt(1);
}
} catch (SQLException e) {
LOGGER.error("Failed to insert wheel prize", e);
}
return 0;
}
/**
* Soft-deletes every enabled prize whose id is not in {@code keptIds} by
* setting {@code enabled = 0}. This is intentionally non-destructive: rows
* stay in the table (so historical references and re-enabling remain
* possible) but {@link #loadPrizes()} only ever loads {@code enabled = 1}.
* An empty set disables all prizes.
*/
public void disablePrizesNotIn(Set<Integer> keptIds) {
if (keptIds == null) return;
StringBuilder sql = new StringBuilder("UPDATE wheel_prizes SET enabled = 0 WHERE enabled = 1");
if (!keptIds.isEmpty()) {
StringJoiner ids = new StringJoiner(",", " AND id NOT IN (", ")");
for (Integer keptId : keptIds) {
ids.add(Integer.toString(keptId));
}
sql.append(ids);
}
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(sql.toString())) {
statement.executeUpdate();
} catch (SQLException e) {
LOGGER.error("Failed to save wheel prize {}", id, e);
LOGGER.error("Failed to disable removed wheel prizes", e);
}
}
@@ -39,6 +39,7 @@ import com.eu.habbo.messages.incoming.hotelview.*;
import com.eu.habbo.messages.incoming.inventory.*;
import com.eu.habbo.messages.incoming.inventory.nickicons.*;
import com.eu.habbo.messages.incoming.inventory.prefixes.*;
import com.eu.habbo.messages.incoming.mentions.*;
import com.eu.habbo.messages.incoming.modtool.*;
import com.eu.habbo.messages.incoming.navigator.*;
import com.eu.habbo.messages.incoming.polls.AnswerPollEvent;
@@ -297,6 +298,8 @@ public class PacketManager {
this.registerHandler(Incoming.CatalogAdminPublishEvent, CatalogAdminPublishEvent.class);
this.registerHandler(Incoming.CatalogAdminSavePageImagesEvent, CatalogAdminSavePageImagesEvent.class);
this.registerHandler(Incoming.CatalogAdminSavePageIconEvent, CatalogAdminSavePageIconEvent.class);
this.registerHandler(Incoming.CatalogAdminLoadOfferEvent, CatalogAdminLoadOfferEvent.class);
this.registerHandler(Incoming.CatalogAdminLoadPageEvent, CatalogAdminLoadPageEvent.class);
}
private void registerEvent() throws Exception {
@@ -426,6 +429,9 @@ public class PacketManager {
}
void registerRooms() throws Exception {
this.registerHandler(Incoming.RequestMentionsEvent, RequestMentionsEvent.class);
this.registerHandler(Incoming.MarkMentionsReadEvent, MarkMentionsReadEvent.class);
this.registerHandler(Incoming.DeleteMentionEvent, DeleteMentionEvent.class);
this.registerHandler(Incoming.RequestRoomLoadEvent, RequestRoomLoadEvent.class);
this.registerHandler(Incoming.RequestHeightmapEvent, RequestRoomHeightmapEvent.class);
this.registerHandler(Incoming.RequestRoomHeightmapEvent, RequestRoomHeightmapEvent.class);
@@ -444,6 +444,8 @@ public class Incoming {
public static final int CatalogAdminPublishEvent = 10058;
public static final int CatalogAdminSavePageImagesEvent = 10060;
public static final int CatalogAdminSavePageIconEvent = 10061;
public static final int CatalogAdminLoadOfferEvent = 10062;
public static final int CatalogAdminLoadPageEvent = 10063;
// Custom Prefixes
public static final int RequestUserPrefixesEvent = 7011;
@@ -496,4 +498,7 @@ public class Incoming {
public static final int WheelAdminSavePrizesEvent = 9305;
public static final int SoundboardPlayEvent = 9306;
public static final int SoundboardSetEnabledEvent = 9307;
public static final int RequestMentionsEvent = 4803;
public static final int MarkMentionsReadEvent = 4804;
public static final int DeleteMentionEvent = 4805;
}
@@ -0,0 +1,55 @@
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogPageType;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminOfferDetailsComposer;
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
public class CatalogAdminLoadOfferEvent extends MessageHandler {
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission"));
return;
}
int offerId = this.packet.readInt();
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
String sql = (pageType == CatalogPageType.BUILDER)
? "SELECT id, order_number FROM catalog_items_bc WHERE id = ? LIMIT 1"
: "SELECT id, offer_id, limited_stack, order_number FROM catalog_items WHERE id = ? LIMIT 1";
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement(sql)) {
statement.setInt(1, offerId);
try (ResultSet set = statement.executeQuery()) {
if (!set.next()) return;
if (pageType == CatalogPageType.BUILDER) {
this.client.sendResponse(new CatalogAdminOfferDetailsComposer(
set.getInt("id"),
0,
0,
set.getInt("order_number")
));
} else {
this.client.sendResponse(new CatalogAdminOfferDetailsComposer(
set.getInt("id"),
set.getInt("offer_id"),
set.getInt("limited_stack"),
set.getInt("order_number")
));
}
}
}
}
}
@@ -0,0 +1,28 @@
package com.eu.habbo.messages.incoming.catalog.catalogadmin;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.habbohotel.catalog.CatalogPageType;
import com.eu.habbo.habbohotel.permissions.Permission;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminPageDetailsComposer;
import com.eu.habbo.messages.outgoing.catalog.catalogadmin.CatalogAdminResultComposer;
public class CatalogAdminLoadPageEvent extends MessageHandler {
@Override
public void handle() throws Exception {
if (!this.client.getHabbo().hasPermission(Permission.ACC_CATALOGFURNI)) {
this.client.sendResponse(new CatalogAdminResultComposer(false, "No permission"));
return;
}
int pageId = this.packet.readInt();
CatalogPageType pageType = CatalogPageType.fromString(this.packet.readString());
CatalogPage page = Emulator.getGameEnvironment().getCatalogManager().getCatalogPage(pageId, pageType);
if (page == null) return;
this.client.sendResponse(new CatalogAdminPageDetailsComposer(page));
}
}
@@ -25,45 +25,55 @@ public class GuildAcceptMembershipEvent extends MessageHandler {
int userId = this.packet.readInt();
Guild guild = Emulator.getGameEnvironment().getGuildManager().getGuild(guildId);
if (guild == null) {
return;
}
GuildMember actorMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
boolean canAccept = guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()
|| this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)
|| (actorMember != null && (actorMember.getRank().equals(GuildRank.ADMIN) || actorMember.getRank().equals(GuildRank.OWNER)));
if (!canAccept) {
return;
}
GuildMember targetMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId);
if (targetMember == null) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.NO_LONGER_MEMBER));
return;
}
if (targetMember.getRank().type != GuildRank.REQUESTED.type) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.ALREADY_ACCEPTED));
return;
}
Habbo habbo = Emulator.getGameEnvironment().getHabboManager().getHabbo(userId);
if (guild != null) {
GuildMember groupMember = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
if (guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()
|| this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)
|| (groupMember != null && (groupMember.getRank().equals(GuildRank.ADMIN) || groupMember.getRank().equals(GuildRank.OWNER)))) {
if (habbo != null) {
if (habbo.getHabboStats().hasGuild(guild.getId())) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.ALREADY_ACCEPTED));
return;
} else {
//Check the user has requested
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, habbo);
if (member == null || member.getRank().type != GuildRank.REQUESTED.type) {
this.client.sendResponse(new GuildAcceptMemberErrorComposer(guild.getId(), GuildAcceptMemberErrorComposer.NO_LONGER_MEMBER));
return;
} else {
GuildAcceptedMembershipEvent event = new GuildAcceptedMembershipEvent(guild, userId, habbo);
Emulator.getPluginManager().fireEvent(event);
if (!event.isCancelled()) {
habbo.getHabboStats().addGuild(guild.getId());
Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, habbo.getHabboInfo().getId(), true);
guild.decreaseRequestCount();
guild.increaseMemberCount();
this.client.sendResponse(new GuildRefreshMembersListComposer(guild));
Room room = habbo.getHabboInfo().getCurrentRoom();
if (room != null) {
if (room.getGuildId() == guildId) {
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId)));
room.refreshRightsForHabbo(habbo);
}
}
}
}
}
} else {
Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, userId, true);
}
GuildAcceptedMembershipEvent event = new GuildAcceptedMembershipEvent(guild, userId, habbo);
Emulator.getPluginManager().fireEvent(event);
if (event.isCancelled()) {
return;
}
if (habbo != null) {
habbo.getHabboStats().addGuild(guild.getId());
}
Emulator.getGameEnvironment().getGuildManager().joinGuild(guild, this.client, userId, true);
guild.decreaseRequestCount();
guild.increaseMemberCount();
this.client.sendResponse(new GuildRefreshMembersListComposer(guild));
if (habbo != null) {
Room room = habbo.getHabboInfo().getCurrentRoom();
if (room != null && room.getGuildId() == guildId) {
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId)));
room.refreshRightsForHabbo(habbo);
}
}
}
@@ -29,6 +29,11 @@ public class GuildDeclineMembershipEvent extends MessageHandler {
if (guild != null) {
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guild, this.client.getHabbo());
if (userId == this.client.getHabbo().getHabboInfo().getId() || guild.getOwnerId() == this.client.getHabbo().getHabboInfo().getId() || (member != null && (member.getRank().equals(GuildRank.ADMIN) || member.getRank().equals(GuildRank.OWNER))) || this.client.getHabbo().hasPermission(Permission.ACC_GUILD_ADMIN)) {
GuildMember target = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, userId);
if (target == null || target.getRank().type != GuildRank.REQUESTED.type) {
return;
}
guild.decreaseRequestCount();
Emulator.getGameEnvironment().getGuildManager().removeMember(guild, userId);
this.client.sendResponse(new GuildMembersComposer(guild, Emulator.getGameEnvironment().getGuildManager().getGuildMembers(guild, 0, 0, ""), this.client.getHabbo(), 0, 0, "", true, Emulator.getGameEnvironment().getGuildManager().getGuildMembersCount(guild, 0, 0, "")));
@@ -30,20 +30,69 @@ public class RequestGuildBuyEvent extends MessageHandler {
final String name = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
final String description = Emulator.getGameEnvironment().getWordFilter().filter(this.packet.readString(), this.client.getHabbo());
if(name.length() > 29){
if (name.length() == 0 || name.length() > 29) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.INVALID_GUILD_NAME));
return;
}
if(description.length() > 254){
if (description.length() > 254) {
return;
}
if (Emulator.getConfig().getBoolean("catalog.guild.hc_required", true) && !this.client.getHabbo().getHabboStats().hasActiveClub()) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.HC_REQUIRED));
return;
}
int roomId = this.packet.readInt();
Room r = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
if (r == null) {
this.client.sendResponse(new AlertPurchaseFailedComposer(AlertPurchaseFailedComposer.SERVER_ERROR));
return;
}
if (r.hasGuild() || r.getGuildId() != 0) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.ROOM_ALREADY_IN_USE));
return;
}
if (r.getOwnerId() != this.client.getHabbo().getHabboInfo().getId()) {
String message = Emulator.getTexts().getValue("scripter.warning.guild.buy.owner").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%roomname%", r.getName().replace("%owner%", r.getOwnerName()));
ScripterManager.scripterDetected(this.client, message);
LOGGER.info(message);
return;
}
int colorOne = this.packet.readInt();
int colorTwo = this.packet.readInt();
int count = this.packet.readInt();
StringBuilder badge = new StringBuilder();
byte base = 1;
while (base < count) {
int id = this.packet.readInt();
int color = this.packet.readInt();
int pos = this.packet.readInt();
if (base == 1) {
badge.append("b");
} else {
badge.append("s");
}
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id).append(color < 10 ? "0" : "").append(color).append(pos);
base += 3;
}
// Only charge the player once every step has been validated. Previously the
// credits were deducted before the room was checked, so a purchase that
// failed afterwards (missing room, room already used by a guild, not the
// owner) still took the credits without ever creating the group.
if (!this.client.getHabbo().hasPermission(Permission.ACC_INFINITE_CREDITS)) {
int guildPrice = Emulator.getConfig().getInt("catalog.guild.price");
if (this.client.getHabbo().getHabboInfo().getCredits() >= guildPrice) {
@@ -54,78 +103,34 @@ public class RequestGuildBuyEvent extends MessageHandler {
}
}
int roomId = this.packet.readInt();
Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge.toString(), colorOne, colorTwo);
Room r = Emulator.getGameEnvironment().getRoomManager().getRoom(roomId);
r.setGuild(guild.getId());
r.removeAllRights();
r.setNeedsUpdate(true);
if (r != null) {
if (r.hasGuild()) {
this.client.sendResponse(new GuildEditFailComposer(GuildEditFailComposer.ROOM_ALREADY_IN_USE));
return;
Emulator.getGameEnvironment().getGuildManager().addGuild(guild);
if (Emulator.getConfig().getBoolean("imager.internal.enabled")) {
Emulator.getBadgeImager().generate(guild);
}
this.client.sendResponse(new PurchaseOKComposer());
this.client.sendResponse(new GuildBoughtComposer(guild));
r.refreshGuild(guild);
for (Habbo habbo : r.getHabbos()) {
if (habbo.getClient() == null) {
continue;
}
if (r.getOwnerId() == this.client.getHabbo().getHabboInfo().getId()) {
if (r.getGuildId() == 0) {
int colorOne = this.packet.readInt();
int colorTwo = this.packet.readInt();
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null));
int count = this.packet.readInt();
StringBuilder badge = new StringBuilder();
byte base = 1;
while (base < count) {
int id = this.packet.readInt();
int color = this.packet.readInt();
int pos = this.packet.readInt();
if (base == 1) {
badge.append("b");
} else {
badge.append("s");
}
badge.append(id < 100 ? "0" : "").append(id < 10 ? "0" : "").append(id).append(color < 10 ? "0" : "").append(color).append(pos);
base += 3;
}
Guild guild = Emulator.getGameEnvironment().getGuildManager().createGuild(this.client.getHabbo(), roomId, r.getName(), name, description, badge.toString(), colorOne, colorTwo);
r.setGuild(guild.getId());
r.removeAllRights();
r.setNeedsUpdate(true);
Emulator.getGameEnvironment().getGuildManager().addGuild(guild);
if (Emulator.getConfig().getBoolean("imager.internal.enabled")) {
Emulator.getBadgeImager().generate(guild);
}
this.client.sendResponse(new PurchaseOKComposer());
this.client.sendResponse(new GuildBoughtComposer(guild));
r.refreshGuild(guild);
for (Habbo habbo : r.getHabbos()) {
if (habbo.getClient() == null) {
continue;
}
habbo.getClient().sendResponse(new GuildInfoComposer(guild, habbo.getClient(), false, null));
if (habbo.getHabboInfo().getId() != this.client.getHabbo().getHabboInfo().getId()) {
habbo.getClient().sendResponse(new RoomDataComposer(r, habbo, true, false));
}
}
Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo()));
}
} else {
String message = Emulator.getTexts().getValue("scripter.warning.guild.buy.owner").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%roomname%", r.getName().replace("%owner%", r.getOwnerName()));
ScripterManager.scripterDetected(this.client, message);
LOGGER.info(message);
if (habbo.getHabboInfo().getId() != this.client.getHabbo().getHabboInfo().getId()) {
habbo.getClient().sendResponse(new RoomDataComposer(r, habbo, true, false));
}
}
Emulator.getPluginManager().fireEvent(new GuildPurchasedEvent(guild, this.client.getHabbo()));
}
}
@@ -2,7 +2,11 @@ package com.eu.habbo.messages.incoming.guilds.forums;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.guilds.Guild;
import com.eu.habbo.habbohotel.guilds.GuildMember;
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;
public class GuildForumDataEvent extends MessageHandler {
@@ -20,10 +24,18 @@ public class GuildForumDataEvent extends MessageHandler {
if (guild == null) return;
if (!guild.hasForum()) return;
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId());
boolean staff = this.client.getHabbo().hasPermission(Permission.ACC_MODTOOL_TICKET_Q);
if (!guild.canHabboReadForum(this.client.getHabbo().getHabboInfo().getId(), member, staff)) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_ACCESS_DENIED.key).compose());
return;
}
this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo()));
if (!Emulator.getGameEnvironment().getGuildManager().hasViewedForum(this.client.getHabbo().getHabboInfo().getId(), guildId)) {
Emulator.getGameEnvironment().getGuildManager().addView(this.client.getHabbo().getHabboInfo().getId(), guildId);
}
}
}
}
@@ -2,7 +2,11 @@ package com.eu.habbo.messages.incoming.guilds.forums;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.guilds.Guild;
import com.eu.habbo.habbohotel.guilds.GuildMember;
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.GuildForumThreadsComposer;
import com.eu.habbo.messages.outgoing.handshake.ConnectionErrorComposer;
@@ -24,8 +28,15 @@ public class GuildForumThreadsEvent extends MessageHandler {
this.client.sendResponse(new ConnectionErrorComposer(404));
return;
}
GuildMember member = Emulator.getGameEnvironment().getGuildManager().getGuildMember(guildId, this.client.getHabbo().getHabboInfo().getId());
boolean staff = this.client.getHabbo().hasPermission(Permission.ACC_MODTOOL_TICKET_Q);
if (!guild.canHabboReadForum(this.client.getHabbo().getHabboInfo().getId(), member, staff)) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_ACCESS_DENIED.key).compose());
return;
}
this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo()));
this.client.sendResponse(new GuildForumThreadsComposer(guild, index));
}
}
}
@@ -38,7 +38,6 @@ public class GuildForumThreadsMessagesEvent extends MessageHandler {
return;
}
// Verify thread belongs to the requested guild
if (thread.getGuildId() != guildId) {
this.client.sendResponse(new ConnectionErrorComposer(403));
return;
@@ -47,6 +46,11 @@ public class GuildForumThreadsMessagesEvent extends MessageHandler {
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)));
if (!guild.canHabboReadForum(this.client.getHabbo().getHabboInfo().getId(), member, hasStaffPermissions)) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FORUMS_ACCESS_DENIED.key).compose());
return;
}
if (thread.getState() != ForumThreadState.HIDDEN_BY_GUILD_ADMIN || hasStaffPermissions || isGuildAdministrator) {
this.client.sendResponse(new GuildForumCommentsComposer(guildId, threadId, index, thread.getComments(limit, index)));
this.client.sendResponse(new GuildForumDataComposer(guild, this.client.getHabbo()));
@@ -1,5 +1,6 @@
package com.eu.habbo.messages.incoming.handshake;
import com.eu.habbo.Emulator;
import com.eu.habbo.messages.NoAuthMessage;
import com.eu.habbo.messages.incoming.MessageHandler;
import org.slf4j.Logger;
@@ -24,6 +25,15 @@ public class MachineIDEvent extends MessageHandler {
this.client.setMachineId(storedMachineId);
// Persist the machine fingerprint onto the user so machine/super bans can
// target it (createOfflineUserBan copies users.machine_id). The Nitro client
// sends this UniqueID packet right after the SSO ticket, so the Habbo is
// normally already loaded by the time we get here.
if (!storedMachineId.isEmpty() && this.client.getHabbo() != null && this.client.getHabbo().getHabboInfo() != null) {
this.client.getHabbo().getHabboInfo().setMachineID(storedMachineId);
Emulator.getThreading().run(this.client.getHabbo());
}
LOGGER.debug("Setting client MachineId to {}", storedMachineId);
}
}
@@ -161,6 +161,12 @@ public class SecureLoginEvent extends MessageHandler {
throw new NullPointerException(habbo.getHabboInfo().getUsername() + " has a NON EXISTING RANK!");
}
// If the machine fingerprint already arrived (UniqueID before login),
// persist it so machine/super bans can target this user.
if (this.client.getMachineId() != null && !this.client.getMachineId().isEmpty()) {
this.client.getHabbo().getHabboInfo().setMachineID(this.client.getMachineId());
}
Emulator.getThreading().run(habbo);
Emulator.getGameEnvironment().getHabboManager().addHabbo(habbo);
} catch (Exception e) {
@@ -6,6 +6,11 @@ import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer;
public class DeletePrefixEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
int prefixId = this.packet.readInt();
@@ -1,15 +1,20 @@
package com.eu.habbo.messages.incoming.inventory.prefixes;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.modtool.WordFilterWord;
import com.eu.habbo.habbohotel.users.Habbo;
import com.eu.habbo.habbohotel.users.UserPrefix;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertComposer;
import com.eu.habbo.messages.outgoing.generic.alerts.BubbleAlertKeys;
import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer;
import com.eu.habbo.messages.outgoing.inventory.prefixes.ActivePrefixUpdatedComposer;
import com.eu.habbo.messages.outgoing.inventory.prefixes.CustomPrefixPurchaseFailedComposer;
import com.eu.habbo.messages.outgoing.inventory.prefixes.PrefixReceivedComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer;
import com.eu.habbo.messages.outgoing.users.UserCreditsComposer;
import com.eu.habbo.messages.outgoing.users.UserCurrencyComposer;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -17,10 +22,18 @@ import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
public class PurchasePrefixEvent extends MessageHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PurchasePrefixEvent.class);
private static final String[] ALLOWED_FONTS = { "", "pixel", "cherry", "vampiro" };
private static final String[] ALLOWED_EFFECTS = {
"", "glow", "shadow", "italic", "outline", "underline", "pulse", "bounce", "wave", "shake",
"discord-neon", "cartoon", "toon", "pop", "bold-glow", "rainbow", "frost", "gold", "glitch",
"fire", "matrix", "sparkle"
};
private static final int MAX_ICON_LENGTH = 16;
@Override
public int getRatelimit() {
@@ -39,81 +52,101 @@ public class PurchasePrefixEvent extends MessageHandler {
if (habbo == null) return;
// Load settings
int maxLength = getSettingInt("max_length", 15);
int minRank = getSettingInt("min_rank_to_buy", 1);
int priceCredits = getSettingInt("price_credits", 5);
int pricePoints = getSettingInt("price_points", 0);
int pointsType = getSettingInt("points_type", 0);
int fontPriceCredits = getSettingInt("font_price_credits", 10);
int fontPricePoints = getSettingInt("font_price_points", 0);
int fontPointsType = getSettingInt("font_points_type", pointsType);
Map<String, Integer> settings = loadSettings();
int maxLength = setting(settings, "max_length", 15);
int minRank = setting(settings, "min_rank_to_buy", 1);
int priceCredits = setting(settings, "price_credits", 5);
int pricePoints = setting(settings, "price_points", 0);
int pointsType = setting(settings, "points_type", 0);
int fontPriceCredits = setting(settings, "font_price_credits", 10);
int fontPricePoints = setting(settings, "font_price_points", 0);
int fontPointsType = setting(settings, "font_points_type", pointsType);
int maxPrefixes = setting(settings, "max_prefixes", 60);
// Validate text
text = text.trim();
if (text.isEmpty() || text.length() > maxLength) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Prefix text is invalid or too long (max " + maxLength + " characters)."));
if (maxPrefixes > 0 && habbo.getInventory().getPrefixesComponent().getPrefixes().size() >= maxPrefixes) {
this.fail(habbo, "You already own the maximum number of prefixes (" + maxPrefixes + ").");
return;
}
text = text.trim();
if (text.isEmpty() || text.length() > maxLength) {
this.fail(habbo, "Prefix text is invalid or too long (max " + maxLength + " characters).");
return;
}
if (containsControlChars(text)) {
this.fail(habbo, "Prefix text contains invalid characters.");
return;
}
if (containsFilteredWord(text)) {
this.fail(habbo, "This prefix contains a blocked word.");
return;
}
// Validate color (single hex or comma-separated multi hex for per-letter colors)
String[] colorParts = color.split(",");
if (colorParts.length > text.length()) {
this.fail(habbo, "Invalid color format.");
return;
}
for (String part : colorParts) {
if (!part.matches("^#[0-9A-Fa-f]{6}$")) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid color format."));
this.fail(habbo, "Invalid color format.");
return;
}
}
// Check rank
if (habbo.getHabboInfo().getRank().getId() < minRank) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Your rank is too low to purchase prefixes."));
return;
}
// Check blacklist
if (isBlacklisted(text)) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "This prefix contains a blocked word."));
this.fail(habbo, "Your rank is too low to purchase prefixes.");
return;
}
if (icon == null) icon = "";
icon = icon.trim();
if (!isValidIcon(icon)) {
this.fail(habbo, "Invalid prefix icon.");
return;
}
if (effect == null) effect = "";
effect = effect.trim();
effect = effect.trim().toLowerCase();
if (!isAllowedEffect(effect)) {
this.fail(habbo, "Invalid prefix effect.");
return;
}
if (font == null) font = "";
font = font.trim().toLowerCase();
if (!isAllowedFont(font)) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Invalid font format."));
this.fail(habbo, "Invalid font format.");
return;
}
int totalPriceCredits = priceCredits + (!font.isEmpty() ? fontPriceCredits : 0);
// Check credits
if (totalPriceCredits > 0 && habbo.getHabboInfo().getCredits() < totalPriceCredits) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough credits."));
this.fail(habbo, "Not enough credits.");
return;
}
int totalPricePointsSameType = pricePoints + ((fontPricePoints > 0 && fontPointsType == pointsType && !font.isEmpty()) ? fontPricePoints : 0);
// Check points
if (totalPricePointsSameType > 0 && habbo.getHabboInfo().getCurrencyAmount(pointsType) < totalPricePointsSameType) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points."));
this.fail(habbo, "Not enough points.");
return;
}
if (!font.isEmpty() && fontPricePoints > 0 && fontPointsType != pointsType && habbo.getHabboInfo().getCurrencyAmount(fontPointsType) < fontPricePoints) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, "Not enough points."));
this.fail(habbo, "Not enough points.");
return;
}
// Deduct currency
if (totalPriceCredits > 0) {
habbo.getHabboInfo().addCredits(-totalPriceCredits);
this.client.sendResponse(new UserCreditsComposer(habbo));
@@ -129,47 +162,57 @@ public class PurchasePrefixEvent extends MessageHandler {
this.client.sendResponse(new UserCurrencyComposer(habbo));
}
// Create prefix
int storedPoints = totalPricePointsSameType;
int storedPointsType = (storedPoints > 0) ? pointsType : ((!font.isEmpty() && fontPricePoints > 0) ? fontPointsType : pointsType);
UserPrefix prefix = new UserPrefix(habbo.getHabboInfo().getId(), text, color, icon, effect, font, 0, text, storedPoints, storedPointsType, true);
prefix.run(); // Insert into DB synchronously to get the ID
habbo.getInventory().getPrefixesComponent().addPrefix(prefix);
habbo.getInventory().getPrefixesComponent().setActive(prefix.getId());
this.client.sendResponse(new PrefixReceivedComposer(prefix));
this.client.sendResponse(new ActivePrefixUpdatedComposer(prefix));
this.client.sendResponse(new UserNickIconsComposer(habbo));
}
private int getSettingInt(String key, int defaultValue) {
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT `value` FROM custom_prefix_settings WHERE key_name = ?")) {
statement.setString(1, key);
try (ResultSet set = statement.executeQuery()) {
if (set.next()) {
return Integer.parseInt(set.getString("value"));
}
}
} catch (SQLException | NumberFormatException e) {
LOGGER.error("Error reading prefix setting: " + key, e);
if (habbo.getHabboInfo().getCurrentRoom() != null) {
habbo.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(habbo).compose());
}
return defaultValue;
}
private boolean isBlacklisted(String text) {
String lowerText = text.toLowerCase();
private void fail(Habbo habbo, String message) {
this.client.sendResponse(new BubbleAlertComposer(BubbleAlertKeys.FURNITURE_PLACEMENT_ERROR.key, message));
this.client.sendResponse(new CustomPrefixPurchaseFailedComposer(message));
}
private Map<String, Integer> loadSettings() {
Map<String, Integer> settings = new HashMap<>();
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
PreparedStatement statement = connection.prepareStatement("SELECT word FROM custom_prefix_blacklist")) {
try (ResultSet set = statement.executeQuery()) {
while (set.next()) {
if (lowerText.contains(set.getString("word").toLowerCase())) {
return true;
}
}
PreparedStatement statement = connection.prepareStatement("SELECT key_name, `value` FROM custom_prefix_settings");
ResultSet set = statement.executeQuery()) {
while (set.next()) {
try {
settings.put(set.getString("key_name"), Integer.parseInt(set.getString("value")));
} catch (NumberFormatException ignored) {}
}
} catch (SQLException e) {
LOGGER.error("Error checking prefix blacklist", e);
LOGGER.error("Error reading prefix settings", e);
}
return settings;
}
private int setting(Map<String, Integer> settings, String key, int defaultValue) {
Integer value = settings.get(key);
return value != null ? value : defaultValue;
}
private boolean containsFilteredWord(String text) {
if (text == null || text.isEmpty()) return false;
for (WordFilterWord word : Emulator.getGameEnvironment().getWordFilter().getWords()) {
if (word.key != null && !word.key.isEmpty() && StringUtils.containsIgnoreCase(text, word.key)) {
return true;
}
}
return false;
}
@@ -182,4 +225,35 @@ public class PurchasePrefixEvent extends MessageHandler {
return false;
}
private boolean isAllowedEffect(String effect) {
for (String allowedEffect : ALLOWED_EFFECTS) {
if (allowedEffect.equals(effect)) {
return true;
}
}
return false;
}
private boolean isValidIcon(String icon) {
if (icon.isEmpty()) return true;
if (icon.length() > MAX_ICON_LENGTH) return false;
for (int i = 0; i < icon.length(); i++) {
char c = icon.charAt(i);
if (c < 0x20 || c == 0x7F || c == '<' || c == '>') return false;
}
return true;
}
private boolean containsControlChars(String text) {
for (int i = 0; i < text.length(); i++) {
char c = text.charAt(i);
if (c < 0x20 || c == 0x7F) return true;
}
return false;
}
}
@@ -4,6 +4,11 @@ import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.inventory.prefixes.UserPrefixesComposer;
public class RequestUserPrefixesEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 500;
}
@Override
public void handle() throws Exception {
this.client.sendResponse(new UserPrefixesComposer(this.client.getHabbo()));
@@ -2,11 +2,16 @@ package com.eu.habbo.messages.incoming.inventory.prefixes;
import com.eu.habbo.habbohotel.users.UserPrefix;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.inventory.prefixes.ActivePrefixUpdatedComposer;
import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer;
import com.eu.habbo.messages.outgoing.inventory.prefixes.ActivePrefixUpdatedComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer;
public class SetActivePrefixEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
int prefixId = this.packet.readInt();
@@ -7,6 +7,11 @@ import com.eu.habbo.messages.outgoing.inventory.nickicons.UserNickIconsComposer;
import com.eu.habbo.messages.outgoing.rooms.users.RoomUserDataComposer;
public class SetDisplayOrderEvent extends MessageHandler {
@Override
public int getRatelimit() {
return 1000;
}
@Override
public void handle() throws Exception {
Habbo habbo = this.client.getHabbo();
@@ -23,4 +28,4 @@ public class SetDisplayOrderEvent extends MessageHandler {
habbo.getHabboInfo().getCurrentRoom().sendComposer(new RoomUserDataComposer(habbo).compose());
}
}
}
}
@@ -0,0 +1,27 @@
package com.eu.habbo.messages.incoming.mentions;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.mentions.MentionManager;
import com.eu.habbo.messages.incoming.MessageHandler;
public class DeleteMentionEvent extends MessageHandler {
@Override
public void handle() throws Exception {
if (this.client == null || this.client.getHabbo() == null) return;
int userId = this.client.getHabbo().getHabboInfo().getId();
int mentionId = this.packet.readInt();
if (mentionId <= 0) {
return;
}
MentionManager manager = Emulator.getGameEnvironment().getMentionManager();
if (!manager.tryAcquireDelete(userId)) {
return;
}
manager.delete(userId, mentionId);
}
}
@@ -0,0 +1,35 @@
package com.eu.habbo.messages.incoming.mentions;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.mentions.MentionManager;
import com.eu.habbo.messages.incoming.MessageHandler;
public class MarkMentionsReadEvent extends MessageHandler {
@Override
public void handle() throws Exception {
if (this.client == null || this.client.getHabbo() == null) return;
int userId = this.client.getHabbo().getHabboInfo().getId();
int mode = this.packet.readInt();
int mentionId = this.packet.readInt();
// Only mode 0 (mark-all) and mode 1 (mark-single) are valid. Reject
// anything else so a crafted packet can't fall into the mark-all branch
// by accident.
if (mode != 0 && mode != 1) {
return;
}
if (mode == 1 && mentionId <= 0) {
return;
}
MentionManager manager = Emulator.getGameEnvironment().getMentionManager();
if (!manager.tryAcquireMarkRead(userId, mode)) {
return;
}
manager.markRead(userId, mode, mentionId);
}
}
@@ -0,0 +1,29 @@
package com.eu.habbo.messages.incoming.mentions;
import com.eu.habbo.Emulator;
import com.eu.habbo.habbohotel.mentions.HabboMention;
import com.eu.habbo.habbohotel.mentions.MentionManager;
import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.mentions.MentionsListComposer;
import java.util.List;
public class RequestMentionsEvent extends MessageHandler {
@Override
public void handle() throws Exception {
if (this.client == null || this.client.getHabbo() == null) return;
int userId = this.client.getHabbo().getHabboInfo().getId();
MentionManager manager = Emulator.getGameEnvironment().getMentionManager();
if (!manager.tryAcquireRequestList(userId)) {
return;
}
int limit = Emulator.getConfig().getInt("mentions.store.limit", 50);
List<HabboMention> mentions = manager.getMentions(userId, limit);
this.client.sendResponse(new MentionsListComposer(mentions));
}
}
@@ -34,6 +34,9 @@ public class RoomUserShoutEvent extends MessageHandler {
if (RoomChatMessage.SAVE_ROOM_CHATS) {
Emulator.getThreading().run(message);
}
Emulator.getGameEnvironment().getMentionManager()
.process(this.client.getHabbo(), this.client.getHabbo().getHabboInfo().getCurrentRoom(), message.getMessage(), RoomChatType.SHOUT);
}
} else {
String reportMessage = Emulator.getTexts().getValue("scripter.warning.chat.length").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%length%", message.getMessage().length() + "");
@@ -36,6 +36,9 @@ public class RoomUserTalkEvent extends MessageHandler {
if (RoomChatMessage.SAVE_ROOM_CHATS) {
Emulator.getThreading().run(message);
}
Emulator.getGameEnvironment().getMentionManager()
.process(this.client.getHabbo(), room, message.getMessage(), RoomChatType.TALK);
}
} else {
String reportMessage = Emulator.getTexts().getValue("scripter.warning.chat.length").replace("%username%", this.client.getHabbo().getHabboInfo().getUsername()).replace("%length%", message.getMessage().length() + "");
@@ -6,6 +6,9 @@ import com.eu.habbo.messages.incoming.MessageHandler;
import com.eu.habbo.messages.outgoing.wheel.WheelAdminPrizesComposer;
import com.eu.habbo.messages.outgoing.wheel.WheelDataComposer;
import java.util.HashSet;
import java.util.Set;
public class WheelAdminSavePrizesEvent extends MessageHandler {
public static final String PERMISSION_KEY = "acc_wheeladmin";
@@ -25,6 +28,12 @@ public class WheelAdminSavePrizesEvent extends MessageHandler {
int count = this.packet.readInt();
if (count <= 0 || count > WheelManager.MAX_PRIZES_PER_SAVE) return;
// The client sends the full authoritative list of prizes in display
// order. id <= 0 means "insert a new prize"; any existing prize whose
// id is absent from this list was removed in the editor and gets
// soft-disabled below.
Set<Integer> keptIds = new HashSet<>();
for (int i = 0; i < count; i++) {
int id = this.packet.readInt();
String type = this.packet.readString();
@@ -33,9 +42,11 @@ public class WheelAdminSavePrizesEvent extends MessageHandler {
int pointsType = this.packet.readInt();
int weight = this.packet.readInt();
String label = this.packet.readString();
wheel.savePrize(id, type, value, amount, pointsType, weight, label);
int savedId = wheel.savePrize(id, type, value, amount, pointsType, weight, label, i);
if (savedId > 0) keptIds.add(savedId);
}
wheel.disablePrizesNotIn(keptIds);
wheel.reload();
this.client.sendResponse(new WheelAdminPrizesComposer(wheel.getPrizes()));
@@ -573,12 +573,15 @@ public class Outgoing {
// Catalog Admin
public static final int CatalogAdminResultComposer = 10059;
public static final int CatalogAdminOfferDetailsComposer = 10062;
public static final int CatalogAdminPageDetailsComposer = 10063;
// Custom Prefixes
public static final int UserPrefixesComposer = 7001;
public static final int PrefixReceivedComposer = 7002;
public static final int ActivePrefixUpdatedComposer = 7003;
public static final int UserNickIconsComposer = 7004;
public static final int CustomPrefixPurchaseFailedComposer = 7005;
public static final int AvailableCommandsComposer = 4050;
// YouTube Room Broadcast
@@ -602,5 +605,7 @@ public class Outgoing {
public static final int WheelAdminPrizesComposer = 9404;
public static final int SoundboardSettingsComposer = 9405;
public static final int SoundboardPlayComposer = 9406;
public static final int MentionReceivedComposer = 4801;
public static final int MentionsListComposer = 4802;
}
@@ -0,0 +1,29 @@
package com.eu.habbo.messages.outgoing.catalog.catalogadmin;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class CatalogAdminOfferDetailsComposer extends MessageComposer {
private final int offerId;
private final int offerIdGroup;
private final int limitedStack;
private final int orderNumber;
public CatalogAdminOfferDetailsComposer(int offerId, int offerIdGroup, int limitedStack, int orderNumber) {
this.offerId = offerId;
this.offerIdGroup = offerIdGroup;
this.limitedStack = limitedStack;
this.orderNumber = orderNumber;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.CatalogAdminOfferDetailsComposer);
this.response.appendInt(this.offerId);
this.response.appendInt(this.offerIdGroup);
this.response.appendInt(this.limitedStack);
this.response.appendInt(this.orderNumber);
return this.response;
}
}
@@ -0,0 +1,27 @@
package com.eu.habbo.messages.outgoing.catalog.catalogadmin;
import com.eu.habbo.habbohotel.catalog.CatalogPage;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class CatalogAdminPageDetailsComposer extends MessageComposer {
private final CatalogPage page;
public CatalogAdminPageDetailsComposer(CatalogPage page) {
this.page = page;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.CatalogAdminPageDetailsComposer);
this.response.appendInt(this.page.getId());
this.response.appendString(this.page.getCaption());
this.response.appendString(this.page.getPageName());
this.response.appendInt(this.page.getRank());
this.response.appendInt(this.page.getOrderNum());
this.response.appendBoolean(this.page.isVisible());
this.response.appendBoolean(this.page.isEnabled());
return this.response;
}
}
@@ -0,0 +1,20 @@
package com.eu.habbo.messages.outgoing.inventory.prefixes;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class CustomPrefixPurchaseFailedComposer extends MessageComposer {
private final String message;
public CustomPrefixPurchaseFailedComposer(String message) {
this.message = message != null ? message : "";
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.CustomPrefixPurchaseFailedComposer);
this.response.appendString(this.message);
return this.response;
}
}
@@ -0,0 +1,29 @@
package com.eu.habbo.messages.outgoing.mentions;
import com.eu.habbo.habbohotel.mentions.HabboMention;
import com.eu.habbo.messages.ServerMessage;
import com.eu.habbo.messages.outgoing.MessageComposer;
import com.eu.habbo.messages.outgoing.Outgoing;
public class MentionReceivedComposer extends MessageComposer {
private final HabboMention mention;
public MentionReceivedComposer(HabboMention mention) {
this.mention = mention;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.MentionReceivedComposer);
this.response.appendInt(this.mention.getId());
this.response.appendInt(this.mention.getSenderUserId());
this.response.appendString(this.mention.getSenderUsername());
this.response.appendString(this.mention.getSenderFigure());
this.response.appendInt(this.mention.getRoomId());
this.response.appendString(this.mention.getRoomName());
this.response.appendString(this.mention.getMessage());
this.response.appendInt(this.mention.getMentionType());
this.response.appendInt(this.mention.getTimestamp());
return this.response;
}
}
@@ -0,0 +1,37 @@
package com.eu.habbo.messages.outgoing.mentions;
import com.eu.habbo.habbohotel.mentions.HabboMention;
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 MentionsListComposer extends MessageComposer {
private final List<HabboMention> mentions;
public MentionsListComposer(List<HabboMention> mentions) {
this.mentions = mentions;
}
@Override
protected ServerMessage composeInternal() {
this.response.init(Outgoing.MentionsListComposer);
this.response.appendInt(this.mentions.size());
for (HabboMention mention : this.mentions) {
this.response.appendInt(mention.getId());
this.response.appendInt(mention.getSenderUserId());
this.response.appendString(mention.getSenderUsername());
this.response.appendString(mention.getSenderFigure());
this.response.appendInt(mention.getRoomId());
this.response.appendString(mention.getRoomName());
this.response.appendString(mention.getMessage());
this.response.appendInt(mention.getMentionType());
this.response.appendInt(mention.getTimestamp());
this.response.appendBoolean(mention.isRead());
}
return this.response;
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ and is developed for free by talented developers and is compatible with the foll
[Latest compiled version](https://github.com/duckietm/Arcturus-Morningstar-Extended/tree/main/Latest_Compiled_Version)
## Connection ##
Use the Websocket plugin!
Use the BUILD-IN Websocket so do NOT load any websocket plugin!
### How do I connect to my emulator using Secure Websockets (wss)?
You have several options to add WSS support to your websocket server.