You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b142d184c | |||
| 867c8ff857 | |||
| 5094d6ce4f | |||
| 2c0ef9873c | |||
| dadc1b8aaf | |||
| 85758b53fa | |||
| 2171b5f2ec | |||
| 46306c8205 | |||
| fadec887cd | |||
| e614c1d64f | |||
| e7deea7d9d | |||
| 44ea3abd4e | |||
| 609cd20ab2 | |||
| 717a7f184f | |||
| 2862446686 | |||
| e97e680006 | |||
| 9f36d95dbc |
@@ -1,26 +1,89 @@
|
||||
CREATE TABLE IF NOT EXISTS `habbo_mentions` (
|
||||
`id` INT NOT NULL AUTO_INCREMENT,
|
||||
`target_user_id` INT NOT NULL,
|
||||
`sender_user_id` INT NOT NULL,
|
||||
`sender_username` VARCHAR(64) NOT NULL DEFAULT '',
|
||||
`room_id` INT NOT NULL,
|
||||
`room_name` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`message` VARCHAR(255) NOT NULL DEFAULT '',
|
||||
`mention_type` TINYINT NOT NULL DEFAULT 0,
|
||||
`timestamp` INT NOT NULL DEFAULT 0,
|
||||
`read` TINYINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (`id`),
|
||||
INDEX `idx_target_read` (`target_user_id`, `read`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`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 IGNORE INTO `emulator_settings` (`key`, `value`) VALUES
|
||||
('mentions.enabled', '1'),
|
||||
('mentions.room.aliases', 'amici,friends,all,everyone,tutti,room,stanza'),
|
||||
('mentions.max.targets', '50'),
|
||||
('mentions.cooldown.ms', '3000'),
|
||||
('mentions.store.limit', '50');
|
||||
|
||||
|
||||
|
||||
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`;
|
||||
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.';
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.eu.habbo</groupId>
|
||||
<artifactId>Habbo</artifactId>
|
||||
<version>4.2.32</version>
|
||||
<version>4.2.37</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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()});
|
||||
|
||||
+25
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public class HabboMention {
|
||||
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");
|
||||
@@ -33,6 +34,16 @@ public class HabboMention {
|
||||
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) {
|
||||
@@ -46,6 +57,7 @@ public class HabboMention {
|
||||
this.mentionType = mentionType;
|
||||
this.timestamp = timestamp;
|
||||
this.read = false;
|
||||
this.senderFigure = sender.getHabboInfo().getLook();
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
@@ -87,4 +99,8 @@ public class HabboMention {
|
||||
public boolean isRead() {
|
||||
return this.read;
|
||||
}
|
||||
|
||||
public String getSenderFigure() {
|
||||
return this.senderFigure == null ? "" : this.senderFigure;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,11 +19,8 @@ public class MentionManager {
|
||||
|
||||
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<>();
|
||||
|
||||
// Per-user request rate limits for the incoming packets that hit the DB.
|
||||
private final ConcurrentHashMap<Integer, Long> requestListCooldowns = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Long> markReadCooldowns = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<Integer, Long> markAllCooldowns = new ConcurrentHashMap<>();
|
||||
@@ -36,20 +33,14 @@ public class MentionManager {
|
||||
return Emulator.getConfig().getInt("mentions.enabled", 1) == 1;
|
||||
}
|
||||
|
||||
/** Broadcast category resolved from a mention alias. */
|
||||
public enum BroadcastScope {
|
||||
NONE,
|
||||
// @room / @stanza - reaches the people currently in the room.
|
||||
ROOM,
|
||||
// @friends / @amici - reaches the sender's online friends, requires acc_mention_friends.
|
||||
FRIENDS,
|
||||
// @all / @everyone / @tutti - reaches every online user, requires acc_mention_everyone.
|
||||
EVERYONE
|
||||
}
|
||||
|
||||
/** Permission key (DB column) required to send an "everyone" broadcast. */
|
||||
public static final String PERMISSION_EVERYONE = "acc_mention_everyone";
|
||||
/** Permission key (DB column) required to send a "friends" broadcast. */
|
||||
public static final String PERMISSION_FRIENDS = "acc_mention_friends";
|
||||
|
||||
private Set<String> parseAliases(String configKey, String defaultValue) {
|
||||
@@ -76,14 +67,6 @@ public class MentionManager {
|
||||
return parseAliases("mentions.everyone.aliases", "all,everyone,tutti");
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify an alias candidate (lowercased, punctuation-trimmed) into a
|
||||
* broadcast scope. {@link BroadcastScope#EVERYONE} wins over
|
||||
* {@link BroadcastScope#FRIENDS} which wins over {@link BroadcastScope#ROOM}
|
||||
* so an admin who's also configured the same word into two lists gets the
|
||||
* most permissive scope (which is also the one requiring the strongest
|
||||
* permission, so it can't be misused).
|
||||
*/
|
||||
private BroadcastScope classifyAlias(String alias,
|
||||
Set<String> everyone,
|
||||
Set<String> friends,
|
||||
@@ -135,8 +118,6 @@ public class MentionManager {
|
||||
BroadcastScope scope = this.classifyAlias(aliasCandidate, everyoneAliases, friendsAliases, roomAliases);
|
||||
|
||||
if (scope != BroadcastScope.NONE) {
|
||||
// Promote to the strongest detected scope so a message with
|
||||
// both @room and @all routes through the @all permission.
|
||||
if (scope.ordinal() > broadcastScope.ordinal()) {
|
||||
broadcastScope = scope;
|
||||
}
|
||||
@@ -145,9 +126,6 @@ public class MentionManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Gate the broadcast on the matching permission. If the sender does
|
||||
// not have the right to use it, drop the broadcast entirely but
|
||||
// keep processing any direct @nick tokens in the same message.
|
||||
if (broadcastScope == BroadcastScope.EVERYONE && !sender.hasPermission(PERMISSION_EVERYONE)) {
|
||||
broadcastScope = BroadcastScope.NONE;
|
||||
} else if (broadcastScope == BroadcastScope.FRIENDS && !sender.hasPermission(PERMISSION_FRIENDS)) {
|
||||
@@ -158,9 +136,6 @@ public class MentionManager {
|
||||
return;
|
||||
}
|
||||
|
||||
// Stricter cooldown for broadcasts: one @all/@friends/@room expands to
|
||||
// up to mentions.max.targets DB writes and packet sends, so rate-limit it
|
||||
// separately from direct mentions.
|
||||
if (broadcastScope != BroadcastScope.NONE) {
|
||||
long roomCooldownMs = Emulator.getConfig().getInt("mentions.room.cooldown.ms", 15000);
|
||||
Long lastRoom = this.roomBroadcastCooldowns.get(senderId);
|
||||
@@ -171,9 +146,6 @@ public class MentionManager {
|
||||
|
||||
int maxTargets = Emulator.getConfig().getInt("mentions.max.targets", 50);
|
||||
if (maxTargets <= 0) maxTargets = 1;
|
||||
|
||||
// Bound the number of direct tokens we even attempt to resolve so a
|
||||
// crafted message can't push us through the room iteration N times.
|
||||
int maxDirectTokens = Math.min(directTokens.size(), maxTargets);
|
||||
|
||||
List<Habbo> targets = new ArrayList<>();
|
||||
@@ -187,7 +159,7 @@ public class MentionManager {
|
||||
this.collectFriendsTargets(sender, senderId, targets, seen, maxTargets);
|
||||
break;
|
||||
case ROOM:
|
||||
this.collectRoomTargets(room, senderId, targets, seen, maxTargets);
|
||||
this.collectRoomTargets(room, senderId, targets, seen, maxTargets, true);
|
||||
break;
|
||||
case NONE:
|
||||
default:
|
||||
@@ -198,6 +170,9 @@ public class MentionManager {
|
||||
if (habbo == null || habbo.getHabboInfo().getId() == senderId) {
|
||||
continue;
|
||||
}
|
||||
if (!acceptsMention(habbo, false)) {
|
||||
continue;
|
||||
}
|
||||
if (seen.add(habbo.getHabboInfo().getId())) {
|
||||
targets.add(habbo);
|
||||
}
|
||||
@@ -229,9 +204,10 @@ public class MentionManager {
|
||||
}
|
||||
}
|
||||
|
||||
private void collectRoomTargets(Room room, int senderId, List<Habbo> targets, Set<Integer> seen, int maxTargets) {
|
||||
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;
|
||||
}
|
||||
@@ -246,6 +222,7 @@ public class MentionManager {
|
||||
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;
|
||||
}
|
||||
@@ -254,11 +231,21 @@ public class MentionManager {
|
||||
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(
|
||||
@@ -281,9 +268,6 @@ public class MentionManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Don't push a notification to the client when the INSERT did not
|
||||
// return an id - the client dedup keys on id and a 0 would skip
|
||||
// dedup entirely, opening a flood path on the next packet.
|
||||
if (generatedId <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -304,7 +288,7 @@ public class MentionManager {
|
||||
if (limit > 200) limit = 200;
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement(
|
||||
"SELECT * FROM habbo_mentions WHERE target_user_id = ? ORDER BY id DESC LIMIT ?")) {
|
||||
"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()) {
|
||||
@@ -319,7 +303,6 @@ public class MentionManager {
|
||||
}
|
||||
|
||||
public void markRead(int userId, int mode, int mentionId) {
|
||||
// Caller has already validated mode and mentionId; this method is defensive only.
|
||||
if (mode != 0 && mode != 1) return;
|
||||
if (mode == 1 && mentionId <= 0) return;
|
||||
|
||||
@@ -351,20 +334,11 @@ public class MentionManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-user rate limit for {@code RequestMentionsEvent}. Returns true when
|
||||
* the caller should be served, false when it must be silently dropped.
|
||||
*/
|
||||
public boolean tryAcquireRequestList(int userId) {
|
||||
long cooldownMs = Emulator.getConfig().getInt("mentions.request.cooldown.ms", 2000);
|
||||
return tryAcquire(this.requestListCooldowns, userId, cooldownMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-user rate limit for {@code MarkMentionsReadEvent}. The mark-single
|
||||
* path (mode == 1) is cheap and gets a short window; the mark-all path
|
||||
* (mode != 1) is a bulk UPDATE and gets a longer one.
|
||||
*/
|
||||
public boolean tryAcquireMarkRead(int userId, int mode) {
|
||||
long cooldownMs;
|
||||
ConcurrentHashMap<Integer, Long> bucket;
|
||||
@@ -378,9 +352,6 @@ public class MentionManager {
|
||||
return tryAcquire(bucket, userId, cooldownMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-user rate limit for {@code DeleteMentionEvent}.
|
||||
*/
|
||||
public boolean tryAcquireDelete(int userId) {
|
||||
long cooldownMs = Emulator.getConfig().getInt("mentions.delete.cooldown.ms", 500);
|
||||
return tryAcquire(this.deleteCooldowns, userId, cooldownMs);
|
||||
@@ -397,11 +368,6 @@ public class MentionManager {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Periodically drop cooldown entries older than the largest window so the
|
||||
* maps don't accumulate one entry per user-that-ever-played for the entire
|
||||
* server lifetime.
|
||||
*/
|
||||
private void pruneCooldownsIfDue(long now) {
|
||||
if (now - this.lastPrune < PRUNE_INTERVAL_MS) return;
|
||||
this.lastPrune = now;
|
||||
@@ -448,20 +414,49 @@ public class MentionManager {
|
||||
return value.substring(0, max);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a present room occupant from a raw mention token. Tries the token
|
||||
* verbatim first (so usernames containing allowed punctuation such as '-',
|
||||
* '.', '!' still match), then falls back to a trailing-punctuation-trimmed
|
||||
* form so a mention written as "@Bob!" still resolves the user "Bob".
|
||||
*/
|
||||
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)) {
|
||||
return room.getHabbo(trimmed);
|
||||
habbo = room.getHabbo(trimmed);
|
||||
if (habbo != null) {
|
||||
return habbo;
|
||||
}
|
||||
return habboManager.getHabbo(trimmed);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -298,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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+55
@@ -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")
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+28
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -573,6 +573,8 @@ 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;
|
||||
|
||||
+29
@@ -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;
|
||||
}
|
||||
}
|
||||
+27
@@ -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;
|
||||
}
|
||||
}
|
||||
+1
@@ -18,6 +18,7 @@ public class MentionReceivedComposer extends MessageComposer {
|
||||
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());
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ public class MentionsListComposer extends MessageComposer {
|
||||
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());
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user