From 6d49b93869579fc7b6de1247d26bf253e0695849 Mon Sep 17 00:00:00 2001 From: MaksSlyzar Date: Sun, 29 Mar 2026 14:14:54 +0300 Subject: [PATCH] notifications, chat update --- game-server/src/game/ChatManager.js | 70 +++++++++++ game-server/src/game/NotificationManager.js | 34 ++++++ game-server/src/models/Friend.js | 16 +++ game-server/src/models/Message.js | 23 ++++ game-server/src/models/Notification.js | 29 +++++ game-server/src/models/associations.js | 27 +++++ game-server/src/models/index.js | 4 + .../src/sockets/handlers/chatHandler.js | 63 ++++++++++ .../src/sockets/handlers/friendHandler.js | 114 ++++++++++++++++++ .../sockets/handlers/notificationHandler.js | 51 ++++++++ game-server/src/sockets/socket.js | 6 + 11 files changed, 437 insertions(+) create mode 100644 game-server/src/game/ChatManager.js create mode 100644 game-server/src/game/NotificationManager.js create mode 100644 game-server/src/models/Friend.js create mode 100644 game-server/src/models/Message.js create mode 100644 game-server/src/models/Notification.js create mode 100644 game-server/src/models/associations.js create mode 100644 game-server/src/sockets/handlers/chatHandler.js create mode 100644 game-server/src/sockets/handlers/friendHandler.js create mode 100644 game-server/src/sockets/handlers/notificationHandler.js diff --git a/game-server/src/game/ChatManager.js b/game-server/src/game/ChatManager.js new file mode 100644 index 0000000..0e79a4f --- /dev/null +++ b/game-server/src/game/ChatManager.js @@ -0,0 +1,70 @@ +const Player = require("../models/Player"); +const { Op } = require("sequelize"); +const Message = require("../models/Message.js"); + +class ChatManager { + async getGlobalHistory(limit = 50) { + try { + return await Message.findAll({ + where: { type: "global" }, + limit, + order: [["createdAt", "ASC"]], + include: [ + { + model: Player, + as: "sender", + attributes: ["username"], + }, + ], + }); + } catch (error) { + console.error("History Error:", error); + return []; + } + } + + async saveMessage({ content, type, senderId, receiverId = null }) { + try { + const newMessage = await Message.create({ + content, + type, + senderId, + receiverId, + }); + const sender = await Player.findByPk(senderId, { + attributes: ["username"], + }); + + return { + id: newMessage.id, + content: newMessage.content, + type: newMessage.type, + senderId: newMessage.senderId, + senderName: sender ? sender.username : "Unknown", + receiverId: newMessage.receiverId, + createdAt: newMessage.createdAt, + }; + } catch (error) { + console.error("Save Message Error:", error); + throw error; + } + } + + async searchPlayers(query, excludeId) { + try { + return await Player.findAll({ + where: { + username: { [Op.like]: `%${query}%` }, + id: { [Op.ne]: excludeId }, + }, + attributes: ["id", "username", "level"], + limit: 10, + }); + } catch (error) { + console.error("Search Error:", error); + return []; + } + } +} + +module.exports = new ChatManager(); diff --git a/game-server/src/game/NotificationManager.js b/game-server/src/game/NotificationManager.js new file mode 100644 index 0000000..62e3da4 --- /dev/null +++ b/game-server/src/game/NotificationManager.js @@ -0,0 +1,34 @@ +const Notification = require("../models/Notification"); + +class NotificationManager { + async createNotification({ playerId, type, title, message, data = {} }) { + try { + return await Notification.create({ + playerId, + type, + title, + message, + data, + }); + } catch (error) { + console.error("Notify Error:", error); + } + } + + async getPlayerNotifications(playerId) { + return await Notification.findAll({ + where: { playerId }, + order: [["createdAt", "DESC"]], + limit: 20, + }); + } + + async markAsRead(notificationId) { + return await Notification.update( + { isRead: true }, + { where: { id: notificationId } }, + ); + } +} + +module.exports = new NotificationManager(); diff --git a/game-server/src/models/Friend.js b/game-server/src/models/Friend.js new file mode 100644 index 0000000..c4a698b --- /dev/null +++ b/game-server/src/models/Friend.js @@ -0,0 +1,16 @@ +const { DataTypes } = require("sequelize"); +const sequelize = require("../config/db"); + +const Friend = sequelize.define("Friend", { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, + }, + status: { + type: DataTypes.ENUM("pending", "accepted"), + defaultValue: "pending", + }, +}); + +module.exports = Friend; diff --git a/game-server/src/models/Message.js b/game-server/src/models/Message.js new file mode 100644 index 0000000..e205621 --- /dev/null +++ b/game-server/src/models/Message.js @@ -0,0 +1,23 @@ +const { DataTypes } = require("sequelize"); +const sequelize = require("../config/db"); + +const Message = sequelize.define("Message", { + content: { + type: DataTypes.TEXT, + allowNull: false, + }, + type: { + type: DataTypes.ENUM("global", "private"), + defaultValue: "global", + }, + senderId: { + type: DataTypes.STRING, + allowNull: false, + }, + receiverId: { + type: DataTypes.STRING, + allowNull: true, + }, +}); + +module.exports = Message; diff --git a/game-server/src/models/Notification.js b/game-server/src/models/Notification.js new file mode 100644 index 0000000..47e1ba9 --- /dev/null +++ b/game-server/src/models/Notification.js @@ -0,0 +1,29 @@ +const { DataTypes } = require("sequelize"); +const sequelize = require("../config/db"); + +const Notification = sequelize.define("Notification", { + playerId: { + type: DataTypes.STRING, + allowNull: false, + }, + type: { + type: DataTypes.ENUM("friend_request", "crafting", "system", "trade"), + allowNull: false, + }, + title: { + type: DataTypes.STRING, + allowNull: false, + }, + message: { + type: DataTypes.TEXT, + }, + data: { + type: DataTypes.JSON, + }, + isRead: { + type: DataTypes.BOOLEAN, + defaultValue: false, + }, +}); + +module.exports = Notification; diff --git a/game-server/src/models/associations.js b/game-server/src/models/associations.js new file mode 100644 index 0000000..a5addd5 --- /dev/null +++ b/game-server/src/models/associations.js @@ -0,0 +1,27 @@ +const Player = require("./Player"); +const Message = require("./Message.js"); +const Friend = require("./Friend"); +const setupAssociations = () => { + Message.belongsTo(Player, { + as: "sender", + foreignKey: "senderId", + }); + + Message.belongsTo(Player, { + as: "receiver", + foreignKey: "receiverId", + }); + + Player.hasMany(Message, { + foreignKey: "senderId", + }); + + Player.belongsToMany(Player, { + through: Friend, + as: "Friends", + foreignKey: "playerId", + otherKey: "friendId", + }); +}; + +module.exports = setupAssociations; diff --git a/game-server/src/models/index.js b/game-server/src/models/index.js index 2f7b6cb..cc0076f 100644 --- a/game-server/src/models/index.js +++ b/game-server/src/models/index.js @@ -1,12 +1,16 @@ const sequelize = require("../config/db"); const Player = require("./Player"); const Inventory = require("./Inventory"); +const setupAssociations = require("./associations"); +const Notification = require("./Notification"); Player.hasMany(Inventory, { foreignKey: "playerId", as: "inventory" }); Inventory.belongsTo(Player, { foreignKey: "playerId" }); +setupAssociations(); module.exports = { sequelize, Player, Inventory, + Notification, }; diff --git a/game-server/src/sockets/handlers/chatHandler.js b/game-server/src/sockets/handlers/chatHandler.js new file mode 100644 index 0000000..d9cb627 --- /dev/null +++ b/game-server/src/sockets/handlers/chatHandler.js @@ -0,0 +1,63 @@ +const chatManager = require("../../game/ChatManager"); + +module.exports = (io, socket) => { + socket.on("chat:get_global_history", async () => { + const history = await chatManager.getGlobalHistory(); + const formattedHistory = history.map((m) => ({ + id: m.id, + content: m.content, + senderName: m.sender?.username || "Unknown", + senderId: m.senderId, + createdAt: m.createdAt, + type: m.type, + })); + socket.emit("chat:global_history", formattedHistory); + }); + socket.on("chat:get_global_history", async () => { + console.log(`Player ${socket.player?.id} requested chat history`); + const history = await chatManager.getGlobalHistory(); + + const formattedHistory = history.map((m) => ({ + id: m.id, + content: m.content, + senderName: m.sender?.username || "Unknown", + senderId: m.senderId, + createdAt: m.createdAt, + type: m.type, + })); + + socket.emit("chat:global_history", formattedHistory); + }); + socket.on("chat:send_message", async (payload) => { + try { + const senderId = socket.user?.id; + if (!senderId) return; + const messageData = await chatManager.saveMessage({ + content: payload.content, + type: payload.type || "global", + senderId: senderId, + receiverId: payload.receiverId, + }); + + if (messageData.type === "global") { + io.emit("chat:new_message", messageData); + } else { + socket + .to(`user_${payload.receiverId}`) + .emit("chat:new_message", messageData); + socket.emit("chat:new_message", messageData); + } + } catch (err) { + console.log(err); + socket.emit("error", { message: "CHAT_SEND_ERROR" }); + } + }); + + socket.on("player:search", async ({ query }) => { + const senderId = socket.player?.id; + if (!query || !senderId) return; + + const results = await chatManager.searchPlayers(query, senderId); + socket.emit("player:search_results", results); + }); +}; diff --git a/game-server/src/sockets/handlers/friendHandler.js b/game-server/src/sockets/handlers/friendHandler.js new file mode 100644 index 0000000..6065315 --- /dev/null +++ b/game-server/src/sockets/handlers/friendHandler.js @@ -0,0 +1,114 @@ +const Player = require("../../models/Player"); +const Friend = require("../../models/Friend"); +const NotificationManager = require("../../game/NotificationManager"); +const Notification = require("../../models/Notification"); + +module.exports = (io, socket) => { + socket.on("player:search", async ({ query }) => { + try { + const players = await Player.findAll({ + where: { + username: { [require("sequelize").Op.like]: `%${query}%` }, + id: { [require("sequelize").Op.ne]: socket.user.id }, + }, + limit: 5, + attributes: ["id", "username", "level"], + }); + socket.emit("player:search_results", players); + } catch (e) { + console.error(e); + } + }); + + socket.on("friend:add", async ({ friendId }) => { + try { + const player = socket.user; + + await NotificationManager.createNotification({ + playerId: friendId, + type: "friend_request", + title: "NEW FRIEND REQUEST", + message: `${player.username} wants to add you as a friend.`, + data: { fromId: player.id }, + }); + + io.to(friendId).emit("notification:new", { + type: "friend_request", + title: "NEW FRIEND REQUEST", + message: `${player.username} wants to add you as a friend.`, + data: { fromId: player.id }, + createdAt: new Date(), + }); + } catch (e) { + socket.emit("error", { message: "FAILED_TO_SEND_REQUEST" }); + } + }); + + socket.on("friend:accept", async ({ friendId, notificationId }) => { + try { + const myId = socket.user.id; + + const exists = await Friend.findOne({ + where: { playerId: myId, friendId: friendId }, + }); + + if (!exists) { + await Friend.bulkCreate([ + { playerId: myId, friendId: friendId }, + { playerId: friendId, friendId: myId }, + ]); + + await Notification.destroy({ + where: { id: notificationId, playerId: myId }, + }); + + const myUpdated = await Player.findByPk(myId, { + include: [ + { + model: Player, + as: "Friends", + attributes: ["id", "username", "level"], + }, + ], + }); + socket.emit("friend:list", myUpdated.Friends || []); + + const friendUpdated = await Player.findByPk(friendId, { + include: [ + { + model: Player, + as: "Friends", + attributes: ["id", "username", "level"], + }, + ], + }); + io.to(friendId).emit("friend:list", friendUpdated.Friends || []); + + const unreadCount = await Notification.count({ + where: { playerId: myId, isRead: false }, + }); + socket.emit("notifications:unread_count", unreadCount); + } + } catch (e) { + console.error(e); + socket.emit("error", { message: "FAILED_TO_ACCEPT_FRIEND" }); + } + }); + + socket.on("friend:get_list", async () => { + try { + const player = await Player.findByPk(socket.user.id, { + include: [ + { + model: Player, + as: "Friends", + attributes: ["id", "username", "level"], + }, + ], + }); + socket.emit("friend:list", player.Friends || []); + } catch (e) { + console.error(e); + } + }); +}; diff --git a/game-server/src/sockets/handlers/notificationHandler.js b/game-server/src/sockets/handlers/notificationHandler.js new file mode 100644 index 0000000..0f0e1e0 --- /dev/null +++ b/game-server/src/sockets/handlers/notificationHandler.js @@ -0,0 +1,51 @@ +const Notification = require("../../models/Notification"); + +module.exports = (io, socket) => { + socket.on("notifications:get_all", async () => { + try { + const list = await Notification.findAll({ + where: { playerId: socket.user.id }, + order: [["createdAt", "DESC"]], + limit: 50, + }); + + socket.emit("notifications:list", list); + + const unreadCount = list.filter((n) => !n.isRead).length; + socket.emit("notifications:unread_count", unreadCount); + } catch (e) { + console.error("Fetch notifications error:", e); + } + }); + + socket.on("notification:read", async ({ id }) => { + try { + await Notification.update( + { isRead: true }, + { where: { id, playerId: socket.user.id } }, + ); + + const unreadCount = await Notification.count({ + where: { playerId: socket.user.id, isRead: false }, + }); + socket.emit("notifications:unread_count", unreadCount); + } catch (e) { + console.error("Read notification error:", e); + } + }); + + socket.on("notification:dismiss", async ({ id }) => { + try { + await Notification.destroy({ + where: { id, playerId: socket.player.id }, + }); + + const unreadCount = await Notification.count({ + where: { playerId: socket.player.id, isRead: false }, + }); + socket.emit("notifications:unread_count", unreadCount); + } catch (e) { + console.error("Dismiss notification error:", e); + } + }); +}; diff --git a/game-server/src/sockets/socket.js b/game-server/src/sockets/socket.js index 6bfb90d..1f2435a 100644 --- a/game-server/src/sockets/socket.js +++ b/game-server/src/sockets/socket.js @@ -4,6 +4,9 @@ const inventoryHandler = require("./handlers/inventoryHandler"); const craftingHandler = require("./handlers/craftingHandler"); const adminHandler = require("./handlers/adminHandler"); const dungeonHandler = require("./handlers/dungeonHandler"); +const chatHandler = require("./handlers/chatHandler"); +const friendHandler = require("./handlers/friendHandler"); +const notificationHandler = require("./handlers/notificationHandler"); const initSockets = (io) => { io.use(socketAuth); @@ -14,6 +17,9 @@ const initSockets = (io) => { craftingHandler(io, socket); adminHandler(io, socket); dungeonHandler(io, socket); + chatHandler(io, socket); + friendHandler(io, socket); + notificationHandler(io, socket); }); };