feat: 2.3.1

This commit is contained in:
2026-05-11 17:24:47 +08:00
parent 57e56412e0
commit 13f3c3d591
733 changed files with 17957 additions and 798 deletions
+328
View File
@@ -19,6 +19,7 @@ function InitModule(ctx, logger, nk, initializer) {
initializer.registerRpc("admin_list_users", rpcAdminListUsers);
initializer.registerRpc("admin_delete_users", rpcAdminDeleteUsers);
initializer.registerRpc("admin_topup_gold", rpcAdminTopupGold);
initializer.registerRpc("admin_clear_global_chat", rpcAdminClearGlobalChat);
// User management RPCs
initializer.registerRpc("get_user_profile", rpcGetUserProfile);
@@ -49,6 +50,12 @@ function InitModule(ctx, logger, nk, initializer) {
initializer.registerRpc("set_daily_reward_config", rpcSetDailyRewardConfig);
initializer.registerRpc("get_daily_reward_config_admin", rpcGetDailyRewardConfigAdmin);
// Inbox System RPCs
initializer.registerRpc("admin_send_mail", rpcAdminSendMail);
initializer.registerRpc("get_mail", rpcGetMail);
initializer.registerRpc("claim_mail_reward", rpcClaimMailReward);
initializer.registerRpc("delete_mail", rpcDeleteMail);
// Steam auth hooks
initializer.registerAfterAuthenticateSteam(afterAuthenticateSteam);
@@ -531,6 +538,53 @@ function rpcAdminTopupGold(ctx, logger, nk, payload) {
}
}
// =============================================================================
// Admin Clear Global Chat RPC
// =============================================================================
function rpcAdminClearGlobalChat(ctx, logger, nk, payload) {
requireAdmin(ctx, nk);
// Nakama's channel message list uses the channel ID, not the room name.
// We need to find the channel ID for "social_global" Room type.
// Room channel IDs are deterministic: we can list messages by cursor.
var req = JSON.parse(payload || "{}");
var channelId = req.channel_id || "";
if (!channelId) {
throw new Error("channel_id is required. Pass the channel ID from the client.");
}
var deleted = 0;
var cursor = "";
try {
// Paginate through all messages and remove each one
do {
var result = nk.channelMessagesList(channelId, 100, false, cursor);
var messages = result.messages || [];
for (var i = 0; i < messages.length; i++) {
try {
nk.channelMessageRemove(channelId, messages[i].messageId);
deleted++;
} catch (e2) {
logger.warn("Failed to remove message " + messages[i].messageId + ": " + e2);
}
}
cursor = result.nextCursor || "";
} while (cursor !== "");
logger.info("[AdminClearGlobalChat] Deleted " + deleted + " messages by " + ctx.userId);
return JSON.stringify({ success: true, deleted: deleted });
} catch (e) {
logger.error("admin_clear_global_chat failed: " + e);
throw new Error("Failed to clear global chat: " + e);
}
}
function rpcBuyCurrency(ctx, logger, nk, payload) {
if (!ctx.userId) throw new Error("Not authenticated");
@@ -1384,3 +1438,277 @@ function rpcSendLobbyInvite(ctx, logger, nk, payload) {
logger.info("Lobby invite sent from " + ctx.userId + " to " + toUserId + " for match " + matchId);
return JSON.stringify({ success: true });
}
// =============================================================================
// Inbox System RPCs
// =============================================================================
function rpcAdminSendMail(ctx, logger, nk, payload) {
requireAdmin(ctx, nk);
var request = JSON.parse(payload || "{}");
var nowStr = new Date().toISOString();
var startDate = request.start_date || nowStr;
var endDate = request.end_date || "";
// Auto-delete / expire after 30 days from start
var startObj = new Date(startDate);
startObj.setDate(startObj.getDate() + 30);
var expiryDate = startObj.toISOString();
var mailObj = {
id: nk.uuidv4(),
title: request.title || "Announcement",
content: request.content || "",
sender: "TEKTON DEV TEAM",
date: startDate,
start_date: startDate,
end_date: endDate,
expiry_date: expiryDate,
rewards: request.rewards || []
};
if (request.target_user_id) {
mailObj.type = "personal";
var invObjs = nk.storageRead([{ collection: "inbox", key: "personal", userId: request.target_user_id }]);
var personalMails = [];
if (invObjs && invObjs.length > 0) {
personalMails = invObjs[0].value.mails || [];
}
personalMails.push(mailObj);
nk.storageWrite([{
collection: "inbox",
key: "personal",
userId: request.target_user_id,
value: { mails: personalMails },
permissionRead: 1,
permissionWrite: 0
}]);
logger.info("Personal mail sent to " + request.target_user_id);
} else {
mailObj.type = "global";
var globalObjs = nk.storageRead([{ collection: "config", key: "global_mail", userId: "00000000-0000-0000-0000-000000000000" }]);
var globalMails = [];
if (globalObjs && globalObjs.length > 0) {
globalMails = globalObjs[0].value.mails || [];
}
globalMails.push(mailObj);
nk.storageWrite([{
collection: "config",
key: "global_mail",
userId: "00000000-0000-0000-0000-000000000000",
value: { mails: globalMails },
permissionRead: 2,
permissionWrite: 0
}]);
logger.info("Global mail sent");
}
return JSON.stringify({ success: true, mail: mailObj });
}
function rpcGetMail(ctx, logger, nk, payload) {
if (!ctx.userId) throw new Error("Not authenticated");
var personalObjs = nk.storageRead([{ collection: "inbox", key: "personal", userId: ctx.userId }]);
var globalObjs = nk.storageRead([{ collection: "config", key: "global_mail", userId: "00000000-0000-0000-0000-000000000000" }]);
var stateObjs = nk.storageRead([{ collection: "inbox", key: "state", userId: ctx.userId }]);
var personalMails = (personalObjs && personalObjs.length > 0) ? (personalObjs[0].value.mails || []) : [];
var globalMails = (globalObjs && globalObjs.length > 0) ? (globalObjs[0].value.mails || []) : [];
var state = { claimed_ids: [], deleted_ids: [], read_ids: [] };
if (stateObjs && stateObjs.length > 0) {
var val = stateObjs[0].value;
state.claimed_ids = val.claimed_ids || [];
state.deleted_ids = val.deleted_ids || [];
state.read_ids = val.read_ids || [];
}
var allMails = personalMails.concat(globalMails);
var filteredMails = [];
var nowStr = new Date().toISOString();
for (var i = 0; i < allMails.length; i++) {
var mail = allMails[i];
if (state.deleted_ids.indexOf(mail.id) !== -1) continue;
// Expiry check
if (mail.expiry_date && nowStr > mail.expiry_date) {
continue;
}
// Scheduled start
if (mail.start_date && nowStr < mail.start_date) {
continue;
}
// Scheduled end
if (mail.type === "global" && mail.end_date && nowStr > mail.end_date) {
continue;
}
filteredMails.push(mail);
}
return JSON.stringify({ mails: filteredMails, state: state });
}
function rpcClaimMailReward(ctx, logger, nk, payload) {
if (!ctx.userId) throw new Error("Not authenticated");
var request = JSON.parse(payload || "{}");
var mailId = request.mail_id;
if (!mailId) throw new Error("mail_id required");
// fetch all mails to find it
var personalObjs = nk.storageRead([{ collection: "inbox", key: "personal", userId: ctx.userId }]);
var globalObjs = nk.storageRead([{ collection: "config", key: "global_mail", userId: "00000000-0000-0000-0000-000000000000" }]);
var stateObjs = nk.storageRead([{ collection: "inbox", key: "state", userId: ctx.userId }]);
var state = { claimed_ids: [], deleted_ids: [], read_ids: [] };
if (stateObjs && stateObjs.length > 0) {
var val = stateObjs[0].value;
state.claimed_ids = val.claimed_ids || [];
state.deleted_ids = val.deleted_ids || [];
state.read_ids = val.read_ids || [];
}
if (state.claimed_ids.indexOf(mailId) !== -1) {
throw new Error("Reward already claimed");
}
var personalMails = (personalObjs && personalObjs.length > 0) ? (personalObjs[0].value.mails || []) : [];
var globalMails = (globalObjs && globalObjs.length > 0) ? (globalObjs[0].value.mails || []) : [];
var allMails = personalMails.concat(globalMails);
var targetMail = null;
for (var i = 0; i < allMails.length; i++) {
if (allMails[i].id === mailId) {
targetMail = allMails[i];
break;
}
}
if (!targetMail) throw new Error("Mail not found");
var rewards = targetMail.rewards || [];
var starTotal = 0;
var goldTotal = 0;
// Support legacy dictionary if it exists
if (!Array.isArray(rewards)) {
starTotal = rewards.star || 0;
goldTotal = rewards.gold || 0;
rewards = []; // prevent array loop
}
var fragsToUpdate = {};
var skinsToAdd = [];
for (var j = 0; j < rewards.length; j++) {
var r = rewards[j];
var type = r.type || "star";
var amount = r.amount || 0;
if (type === "star") starTotal += amount;
else if (type === "gold") goldTotal += amount;
else if (type.startsWith("frag_") || type === "item") {
var fragId = r.id || type;
fragsToUpdate[fragId] = (fragsToUpdate[fragId] || 0) + amount;
}
else if (type === "skin") {
if (r.id) skinsToAdd.push(r.id);
}
}
if (starTotal > 0 || goldTotal > 0) {
var changes = {};
if (starTotal > 0) changes["star"] = starTotal;
if (goldTotal > 0) changes["gold"] = goldTotal;
nk.walletUpdate(ctx.userId, changes, {}, true);
}
if (Object.keys(fragsToUpdate).length > 0) {
var invObjs = nk.storageRead([{ collection: "inventory", key: "fragments", userId: ctx.userId }]);
var frags = {};
if (invObjs && invObjs.length > 0) {
frags = invObjs[0].value;
}
for (var fId in fragsToUpdate) {
frags[fId] = (frags[fId] || 0) + fragsToUpdate[fId];
}
nk.storageWrite([{
collection: "inventory",
key: "fragments",
userId: ctx.userId,
value: frags,
permissionRead: 1,
permissionWrite: 0
}]);
}
if (skinsToAdd.length > 0) {
var skinWrites = [];
for (var s = 0; s < skinsToAdd.length; s++) {
skinWrites.push({
collection: "inventory",
key: skinsToAdd[s],
userId: ctx.userId,
value: { acquired_via: "mail", purchased_at: new Date().toISOString() },
permissionRead: 1,
permissionWrite: 0
});
}
nk.storageWrite(skinWrites);
}
state.claimed_ids.push(mailId);
if (state.read_ids.indexOf(mailId) === -1) {
state.read_ids.push(mailId);
}
nk.storageWrite([{
collection: "inbox",
key: "state",
userId: ctx.userId,
value: state,
permissionRead: 1,
permissionWrite: 0
}]);
return JSON.stringify({ success: true, claimed_ids: state.claimed_ids });
}
function rpcDeleteMail(ctx, logger, nk, payload) {
if (!ctx.userId) throw new Error("Not authenticated");
var request = JSON.parse(payload || "{}");
var mailId = request.mail_id;
if (!mailId) throw new Error("mail_id required");
var stateObjs = nk.storageRead([{ collection: "inbox", key: "state", userId: ctx.userId }]);
var state = { claimed_ids: [], deleted_ids: [], read_ids: [] };
if (stateObjs && stateObjs.length > 0) {
var val = stateObjs[0].value;
state.claimed_ids = val.claimed_ids || [];
state.deleted_ids = val.deleted_ids || [];
state.read_ids = val.read_ids || [];
}
if (state.deleted_ids.indexOf(mailId) === -1) {
state.deleted_ids.push(mailId);
}
if (state.read_ids.indexOf(mailId) === -1) {
state.read_ids.push(mailId);
}
nk.storageWrite([{
collection: "inbox",
key: "state",
userId: ctx.userId,
value: state,
permissionRead: 1,
permissionWrite: 0
}]);
return JSON.stringify({ success: true, deleted_ids: state.deleted_ids });
}