diff --git a/Database Updates/009_mentions.sql b/Database Updates/009_mentions.sql new file mode 100644 index 00000000..d9b72c56 --- /dev/null +++ b/Database Updates/009_mentions.sql @@ -0,0 +1,21 @@ +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; + +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'); diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java index 8fdb7842..8d45809c 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/GameEnvironment.java @@ -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; } diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/mentions/HabboMention.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/mentions/HabboMention.java new file mode 100644 index 00000000..96ff7a4d --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/mentions/HabboMention.java @@ -0,0 +1,90 @@ +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; + + 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; + } + + 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; + } + + 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; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/mentions/MentionManager.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/mentions/MentionManager.java new file mode 100644 index 00000000..904474b5 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/mentions/MentionManager.java @@ -0,0 +1,211 @@ +package com.eu.habbo.habbohotel.mentions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.rooms.Room; +import com.eu.habbo.habbohotel.rooms.RoomChatType; +import com.eu.habbo.habbohotel.users.Habbo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class MentionManager { + + private static final Logger LOGGER = LoggerFactory.getLogger(MentionManager.class); + + private final ConcurrentHashMap cooldowns = new ConcurrentHashMap<>(); + + public boolean isEnabled() { + return Emulator.getConfig().getInt("mentions.enabled", 1) == 1; + } + + private Set roomAliases() { + Set aliases = new HashSet<>(); + String raw = Emulator.getConfig().getValue("mentions.room.aliases", "amici,friends,all,everyone,tutti,room,stanza"); + for (String alias : raw.split(",")) { + String trimmed = alias.trim().toLowerCase(); + if (!trimmed.isEmpty()) { + aliases.add(trimmed); + } + } + return aliases; + } + + 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 aliases = this.roomAliases(); + boolean roomBroadcast = false; + LinkedHashSet directNicks = new LinkedHashSet<>(); + + for (String token : message.split("\\s+")) { + if (token.length() < 2 || token.charAt(0) != '@') { + continue; + } + + String nick = token.substring(1).replaceAll("[^A-Za-z0-9_]", "").toLowerCase(); + if (nick.isEmpty()) { + continue; + } + + if (aliases.contains(nick)) { + roomBroadcast = true; + } else { + directNicks.add(nick); + } + } + + if (!roomBroadcast && directNicks.isEmpty()) { + return; + } + + int maxTargets = Emulator.getConfig().getInt("mentions.max.targets", 50); + + List targets = new ArrayList<>(); + Set seen = new HashSet<>(); + + if (roomBroadcast) { + for (Habbo habbo : room.getHabbos()) { + if (habbo == null || habbo.getHabboInfo().getId() == senderId) { + continue; + } + if (seen.add(habbo.getHabboInfo().getId())) { + targets.add(habbo); + } + if (targets.size() >= maxTargets) { + break; + } + } + } else { + for (String nick : directNicks) { + Habbo habbo = room.getHabbo(nick); + if (habbo == null || habbo.getHabboInfo().getId() == senderId) { + continue; + } + if (seen.add(habbo.getHabboInfo().getId())) { + targets.add(habbo); + } + if (targets.size() >= maxTargets) { + break; + } + } + } + + if (targets.isEmpty()) { + return; + } + + this.cooldowns.put(senderId, now); + + int mentionType = roomBroadcast ? HabboMention.TYPE_ROOM : HabboMention.TYPE_DIRECT; + int timestamp = Emulator.getIntUnixTimestamp(); + String roomName = room.getName(); + + String storedMessage = message; + if (storedMessage.length() > 255) { + storedMessage = storedMessage.substring(0, 255); + } + + 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 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); + } + } + + 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 getMentions(int userId, int limit) { + List mentions = new ArrayList<>(); + try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); + PreparedStatement statement = connection.prepareStatement( + "SELECT * FROM habbo_mentions 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) { + String query = mode == 1 + ? "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ? AND id = ?" + : "UPDATE habbo_mentions SET `read` = 1 WHERE target_user_id = ?"; + 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); + } + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java index 0b54d15e..599c1ad2 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/PacketManager.java @@ -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; @@ -426,6 +427,8 @@ public class PacketManager { } void registerRooms() throws Exception { + this.registerHandler(Incoming.RequestMentionsEvent, RequestMentionsEvent.class); + this.registerHandler(Incoming.MarkMentionsReadEvent, MarkMentionsReadEvent.class); this.registerHandler(Incoming.RequestRoomLoadEvent, RequestRoomLoadEvent.class); this.registerHandler(Incoming.RequestHeightmapEvent, RequestRoomHeightmapEvent.class); this.registerHandler(Incoming.RequestRoomHeightmapEvent, RequestRoomHeightmapEvent.class); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java index 9d372215..b088492a 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/Incoming.java @@ -496,4 +496,6 @@ 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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/mentions/MarkMentionsReadEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/mentions/MarkMentionsReadEvent.java new file mode 100644 index 00000000..5abfddda --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/mentions/MarkMentionsReadEvent.java @@ -0,0 +1,15 @@ +package com.eu.habbo.messages.incoming.mentions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.messages.incoming.MessageHandler; + +public class MarkMentionsReadEvent extends MessageHandler { + @Override + public void handle() throws Exception { + int userId = this.client.getHabbo().getHabboInfo().getId(); + int mode = this.packet.readInt(); + int mentionId = this.packet.readInt(); + + Emulator.getGameEnvironment().getMentionManager().markRead(userId, mode, mentionId); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/mentions/RequestMentionsEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/mentions/RequestMentionsEvent.java new file mode 100644 index 00000000..9878da92 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/mentions/RequestMentionsEvent.java @@ -0,0 +1,19 @@ +package com.eu.habbo.messages.incoming.mentions; + +import com.eu.habbo.Emulator; +import com.eu.habbo.habbohotel.mentions.HabboMention; +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 { + int userId = this.client.getHabbo().getHabboInfo().getId(); + int limit = Emulator.getConfig().getInt("mentions.store.limit", 50); + + List mentions = Emulator.getGameEnvironment().getMentionManager().getMentions(userId, limit); + this.client.sendResponse(new MentionsListComposer(mentions)); + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserShoutEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserShoutEvent.java index 1c0e4fba..57f8c60f 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserShoutEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserShoutEvent.java @@ -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() + ""); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserTalkEvent.java b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserTalkEvent.java index 2201fb4c..8659f971 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserTalkEvent.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/incoming/rooms/users/RoomUserTalkEvent.java @@ -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() + ""); diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java index 882365a4..d311ebed 100644 --- a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/Outgoing.java @@ -602,5 +602,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; } diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/mentions/MentionReceivedComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/mentions/MentionReceivedComposer.java new file mode 100644 index 00000000..b7a446c7 --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/mentions/MentionReceivedComposer.java @@ -0,0 +1,28 @@ +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.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; + } +} diff --git a/Emulator/src/main/java/com/eu/habbo/messages/outgoing/mentions/MentionsListComposer.java b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/mentions/MentionsListComposer.java new file mode 100644 index 00000000..e261fe3c --- /dev/null +++ b/Emulator/src/main/java/com/eu/habbo/messages/outgoing/mentions/MentionsListComposer.java @@ -0,0 +1,36 @@ +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 mentions; + + public MentionsListComposer(List 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.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; + } +}