notifications, chat update
This commit is contained in:
parent
342a674456
commit
6d49b93869
70
game-server/src/game/ChatManager.js
Normal file
70
game-server/src/game/ChatManager.js
Normal file
@ -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();
|
||||||
34
game-server/src/game/NotificationManager.js
Normal file
34
game-server/src/game/NotificationManager.js
Normal file
@ -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();
|
||||||
16
game-server/src/models/Friend.js
Normal file
16
game-server/src/models/Friend.js
Normal file
@ -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;
|
||||||
23
game-server/src/models/Message.js
Normal file
23
game-server/src/models/Message.js
Normal file
@ -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;
|
||||||
29
game-server/src/models/Notification.js
Normal file
29
game-server/src/models/Notification.js
Normal file
@ -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;
|
||||||
27
game-server/src/models/associations.js
Normal file
27
game-server/src/models/associations.js
Normal file
@ -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;
|
||||||
@ -1,12 +1,16 @@
|
|||||||
const sequelize = require("../config/db");
|
const sequelize = require("../config/db");
|
||||||
const Player = require("./Player");
|
const Player = require("./Player");
|
||||||
const Inventory = require("./Inventory");
|
const Inventory = require("./Inventory");
|
||||||
|
const setupAssociations = require("./associations");
|
||||||
|
const Notification = require("./Notification");
|
||||||
|
|
||||||
Player.hasMany(Inventory, { foreignKey: "playerId", as: "inventory" });
|
Player.hasMany(Inventory, { foreignKey: "playerId", as: "inventory" });
|
||||||
Inventory.belongsTo(Player, { foreignKey: "playerId" });
|
Inventory.belongsTo(Player, { foreignKey: "playerId" });
|
||||||
|
setupAssociations();
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
sequelize,
|
sequelize,
|
||||||
Player,
|
Player,
|
||||||
Inventory,
|
Inventory,
|
||||||
|
Notification,
|
||||||
};
|
};
|
||||||
|
|||||||
63
game-server/src/sockets/handlers/chatHandler.js
Normal file
63
game-server/src/sockets/handlers/chatHandler.js
Normal file
@ -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);
|
||||||
|
});
|
||||||
|
};
|
||||||
114
game-server/src/sockets/handlers/friendHandler.js
Normal file
114
game-server/src/sockets/handlers/friendHandler.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
51
game-server/src/sockets/handlers/notificationHandler.js
Normal file
51
game-server/src/sockets/handlers/notificationHandler.js
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -4,6 +4,9 @@ const inventoryHandler = require("./handlers/inventoryHandler");
|
|||||||
const craftingHandler = require("./handlers/craftingHandler");
|
const craftingHandler = require("./handlers/craftingHandler");
|
||||||
const adminHandler = require("./handlers/adminHandler");
|
const adminHandler = require("./handlers/adminHandler");
|
||||||
const dungeonHandler = require("./handlers/dungeonHandler");
|
const dungeonHandler = require("./handlers/dungeonHandler");
|
||||||
|
const chatHandler = require("./handlers/chatHandler");
|
||||||
|
const friendHandler = require("./handlers/friendHandler");
|
||||||
|
const notificationHandler = require("./handlers/notificationHandler");
|
||||||
|
|
||||||
const initSockets = (io) => {
|
const initSockets = (io) => {
|
||||||
io.use(socketAuth);
|
io.use(socketAuth);
|
||||||
@ -14,6 +17,9 @@ const initSockets = (io) => {
|
|||||||
craftingHandler(io, socket);
|
craftingHandler(io, socket);
|
||||||
adminHandler(io, socket);
|
adminHandler(io, socket);
|
||||||
dungeonHandler(io, socket);
|
dungeonHandler(io, socket);
|
||||||
|
chatHandler(io, socket);
|
||||||
|
friendHandler(io, socket);
|
||||||
|
notificationHandler(io, socket);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user