local nk = require("nakama") local utils = require("lua.utils") local inbox = {} function inbox.rpc_admin_send_mail(context, payload) utils.require_admin(context) local request = nk.json_decode(payload or "{}") local nowStr = os.date("!%Y-%m-%dT%H:%M:%S.000Z") -- approximate ISO8601 local startDate = request.start_date or nowStr local endDate = request.end_date or "" -- 30 days from now in seconds for expiry_date fallback if not specified local expiryDate = os.date("!%Y-%m-%dT%H:%M:%S.000Z", os.time() + 30 * 24 * 60 * 60) local mailObj = { id = nk.uuid_v4(), title = request.title or "Announcement", content = request.content or "", sender = "TEKTON DEV TEAM", date = startDate, start_date = startDate, end_date = endDate, expiry_date = expiryDate, rewards = request.rewards or {} } if request.target_user_id and request.target_user_id ~= "" then mailObj.type = "personal" local invObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = request.target_user_id }}) local personalMails = {} if #invObjs > 0 then personalMails = invObjs[1].value.mails or {} end table.insert(personalMails, mailObj) nk.storage_write({{ collection = "inbox", key = "personal", user_id = request.target_user_id, value = { mails = personalMails }, permission_read = 1, permission_write = 0 }}) nk.logger_info("Personal mail sent to " .. request.target_user_id) else mailObj.type = "global" local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }}) local globalMails = {} if #globalObjs > 0 then globalMails = globalObjs[1].value.mails or {} end table.insert(globalMails, mailObj) nk.storage_write({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000", value = { mails = globalMails }, permission_read = 2, permission_write = 0 }}) nk.logger_info("Global mail sent") end return nk.json_encode({ success = true, mail = mailObj }) end function inbox.rpc_get_mail(context, payload) if not context.user_id then error("Not authenticated") end local personalObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = context.user_id }}) local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }}) local stateObjs = nk.storage_read({{ collection = "inbox", key = "state", user_id = context.user_id }}) local personalMails = (#personalObjs > 0) and (personalObjs[1].value.mails or {}) or {} local globalMails = (#globalObjs > 0) and (globalObjs[1].value.mails or {}) or {} local state = { claimed_ids = {}, deleted_ids = {}, read_ids = {} } if #stateObjs > 0 then local val = stateObjs[1].value state.claimed_ids = val.claimed_ids or {} state.deleted_ids = val.deleted_ids or {} state.read_ids = val.read_ids or {} end local function array_contains(arr, val) for _, v in ipairs(arr) do if v == val then return true end end return false end local allMails = {} for _, m in ipairs(personalMails) do table.insert(allMails, m) end for _, m in ipairs(globalMails) do table.insert(allMails, m) end local filteredMails = {} local nowStr = os.date("!%Y-%m-%dT%H:%M:%S.000Z") for _, mail in ipairs(allMails) do if not array_contains(state.deleted_ids, mail.id) then local skip = false if mail.expiry_date and mail.expiry_date ~= "" and nowStr > mail.expiry_date then skip = true end if not skip and mail.start_date and mail.start_date ~= "" and nowStr < mail.start_date then skip = true end if not skip and mail.type == "global" and mail.end_date and mail.end_date ~= "" and nowStr > mail.end_date then skip = true end if not skip then table.insert(filteredMails, mail) end end end return nk.json_encode({ mails = filteredMails, state = state }) end function inbox.rpc_claim_mail_reward(context, payload) if not context.user_id then error("Not authenticated") end local request = nk.json_decode(payload or "{}") local mailId = request.mail_id if not mailId then error("mail_id required") end local personalObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = context.user_id }}) local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }}) local stateObjs = nk.storage_read({{ collection = "inbox", key = "state", user_id = context.user_id }}) local state = { claimed_ids = {}, deleted_ids = {}, read_ids = {} } if #stateObjs > 0 then local val = stateObjs[1].value state.claimed_ids = val.claimed_ids or {} state.deleted_ids = val.deleted_ids or {} state.read_ids = val.read_ids or {} end local function array_contains(arr, val) for _, v in ipairs(arr) do if v == val then return true end end return false end if array_contains(state.claimed_ids, mailId) then error("Reward already claimed") end local personalMails = (#personalObjs > 0) and (personalObjs[1].value.mails or {}) or {} local globalMails = (#globalObjs > 0) and (globalObjs[1].value.mails or {}) or {} local allMails = {} for _, m in ipairs(personalMails) do table.insert(allMails, m) end for _, m in ipairs(globalMails) do table.insert(allMails, m) end local targetMail = nil for _, mail in ipairs(allMails) do if mail.id == mailId then targetMail = mail break end end if not targetMail then error("Mail not found") end local rewards = targetMail.rewards or {} local starTotal = 0 local goldTotal = 0 local fragsToUpdate = {} local skinsToAdd = {} if type(rewards) == "table" and not rewards[1] and (rewards.star or rewards.gold) then -- Handle legacy dictionary format starTotal = rewards.star or 0 goldTotal = rewards.gold or 0 rewards = {} end for _, r in ipairs(rewards) do local rType = r.type or "star" local amount = r.amount or 0 if rType == "star" then starTotal = starTotal + amount elseif rType == "gold" then goldTotal = goldTotal + amount elseif string.sub(rType, 1, 5) == "frag_" or rType == "item" then local fragId = r.id or rType fragsToUpdate[fragId] = (fragsToUpdate[fragId] or 0) + amount elseif rType == "skin" then if r.id then table.insert(skinsToAdd, r.id) end end end if starTotal > 0 or goldTotal > 0 then local changes = {} if starTotal > 0 then changes.star = starTotal end if goldTotal > 0 then changes.gold = goldTotal end nk.wallet_update(context.user_id, changes, {}, true) end local fragKeysCount = 0 for _ in pairs(fragsToUpdate) do fragKeysCount = fragKeysCount + 1 end if fragKeysCount > 0 then local invObjs = nk.storage_read({{ collection = "inventory", key = "fragments", user_id = context.user_id }}) local frags = (#invObjs > 0) and invObjs[1].value or {} for fId, count in pairs(fragsToUpdate) do frags[fId] = (frags[fId] or 0) + count end nk.storage_write({{ collection = "inventory", key = "fragments", user_id = context.user_id, value = frags, permission_read = 1, permission_write = 0 }}) end if #skinsToAdd > 0 then local skinWrites = {} for _, sId in ipairs(skinsToAdd) do table.insert(skinWrites, { collection = "inventory", key = sId, user_id = context.user_id, value = { acquired_via = "mail", purchased_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") }, permission_read = 1, permission_write = 0 }) end nk.storage_write(skinWrites) end table.insert(state.claimed_ids, mailId) if not array_contains(state.read_ids, mailId) then table.insert(state.read_ids, mailId) end nk.storage_write({{ collection = "inbox", key = "state", user_id = context.user_id, value = state, permission_read = 1, permission_write = 0 }}) return nk.json_encode({ success = true, claimed_ids = state.claimed_ids }) end function inbox.rpc_delete_mail(context, payload) if not context.user_id then error("Not authenticated") end local request = nk.json_decode(payload or "{}") local mailId = request.mail_id if not mailId then error("mail_id required") end local stateObjs = nk.storage_read({{ collection = "inbox", key = "state", user_id = context.user_id }}) local state = { claimed_ids = {}, deleted_ids = {}, read_ids = {} } if #stateObjs > 0 then local val = stateObjs[1].value state.claimed_ids = val.claimed_ids or {} state.deleted_ids = val.deleted_ids or {} state.read_ids = val.read_ids or {} end local function array_contains(arr, val) for _, v in ipairs(arr) do if v == val then return true end end return false end if not array_contains(state.deleted_ids, mailId) then table.insert(state.deleted_ids, mailId) end if not array_contains(state.read_ids, mailId) then table.insert(state.read_ids, mailId) end nk.storage_write({{ collection = "inbox", key = "state", user_id = context.user_id, value = state, permission_read = 1, permission_write = 0 }}) return nk.json_encode({ success = true, deleted_ids = state.deleted_ids }) end function inbox.rpc_save_mail_state(context, payload) if not context.user_id then error("Not authenticated") end local request = nk.json_decode(payload or "{}") local stateObjs = nk.storage_read({{ collection = "inbox", key = "state", user_id = context.user_id }}) local state = { claimed_ids = {}, deleted_ids = {}, read_ids = {} } if #stateObjs > 0 then local val = stateObjs[1].value state.claimed_ids = val.claimed_ids or {} state.deleted_ids = val.deleted_ids or {} state.read_ids = val.read_ids or {} end local function array_contains(arr, val) for _, v in ipairs(arr) do if v == val then return true end end return false end local newReadIds = request.read_ids or {} for _, rid in ipairs(newReadIds) do if not array_contains(state.read_ids, rid) then table.insert(state.read_ids, rid) end end nk.storage_write({{ collection = "inbox", key = "state", user_id = context.user_id, value = state, permission_read = 1, permission_write = 0 }}) return nk.json_encode({ success = true }) end function inbox.rpc_admin_list_mail(context, payload) utils.require_admin(context) local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }}) local globalMails = (#globalObjs > 0) and (globalObjs[1].value.mails or {}) or {} for _, m in ipairs(globalMails) do m.type = "global" end local personalMails = {} local cursor = nil repeat local status, listResult = pcall(nk.storage_list, "", "inbox", 100, cursor) if status and listResult then local objects = listResult.objects or {} for _, obj in ipairs(objects) do if obj.key == "personal" then local ownerUserId = obj.user_id local mails = obj.value.mails or {} for _, m in ipairs(mails) do m.type = "personal" m.target_user_id = ownerUserId table.insert(personalMails, m) end end end cursor = listResult.cursor else cursor = nil end until not cursor or cursor == "" local allMails = {} for _, m in ipairs(globalMails) do table.insert(allMails, m) end for _, m in ipairs(personalMails) do table.insert(allMails, m) end table.sort(allMails, function(a, b) local d1 = a.date or "" local d2 = b.date or "" return d1 > d2 end) return nk.json_encode({ mails = allMails }) end function inbox.rpc_admin_update_mail(context, payload) utils.require_admin(context) local request = nk.json_decode(payload or "{}") local mailId = request.mail_id if not mailId then error("mail_id required") end local isGlobal = request.type ~= "personal" local targetUserId = request.target_user_id or "" local newTargetUserId = request.new_target_user_id local hasNewTarget = (newTargetUserId ~= nil) local mailObj = nil if isGlobal then local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }}) local globalMails = (#globalObjs > 0) and (globalObjs[1].value.mails or {}) or {} for i, m in ipairs(globalMails) do if m.id == mailId then mailObj = table.remove(globalMails, i) break end end if not mailObj then error("Mail not found in global") end nk.storage_write({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000", value = { mails = globalMails }, permission_read = 2, permission_write = 0 }}) else if targetUserId == "" then error("target_user_id required for personal mail") end local pObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = targetUserId }}) local personalMails = (#pObjs > 0) and (pObjs[1].value.mails or {}) or {} for i, m in ipairs(personalMails) do if m.id == mailId then mailObj = table.remove(personalMails, i) break end end if not mailObj then error("Mail not found in personal inbox") end nk.storage_write({{ collection = "inbox", key = "personal", user_id = targetUserId, value = { mails = personalMails }, permission_read = 1, permission_write = 0 }}) end if request.title ~= nil then mailObj.title = request.title end if request.content ~= nil then mailObj.content = request.content end if request.end_date ~= nil then mailObj.end_date = request.end_date end if request.expiry_date ~= nil then mailObj.expiry_date = request.expiry_date end local destUserId = "" if hasNewTarget then destUserId = newTargetUserId else if not isGlobal then destUserId = targetUserId end end if destUserId == "" then mailObj.type = "global" local gObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }}) local gMails = (#gObjs > 0) and (gObjs[1].value.mails or {}) or {} table.insert(gMails, mailObj) nk.storage_write({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000", value = { mails = gMails }, permission_read = 2, permission_write = 0 }}) else mailObj.type = "personal" local dObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = destUserId }}) local dMails = (#dObjs > 0) and (dObjs[1].value.mails or {}) or {} table.insert(dMails, mailObj) nk.storage_write({{ collection = "inbox", key = "personal", user_id = destUserId, value = { mails = dMails }, permission_read = 1, permission_write = 0 }}) end nk.logger_info("Admin updated mail " .. mailId .. " by " .. context.user_id) return nk.json_encode({ success = true }) end function inbox.rpc_admin_delete_mail_server(context, payload) utils.require_admin(context) local request = nk.json_decode(payload or "{}") local mailId = request.mail_id if not mailId then error("mail_id required") end local isGlobal = request.type ~= "personal" local targetUserId = request.target_user_id or "" if isGlobal then local globalObjs = nk.storage_read({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000" }}) local globalMails = (#globalObjs > 0) and (globalObjs[1].value.mails or {}) or {} local before = #globalMails local filtered = {} for _, m in ipairs(globalMails) do if m.id ~= mailId then table.insert(filtered, m) end end if #filtered == before then error("Mail not found") end nk.storage_write({{ collection = "config", key = "global_mail", user_id = "00000000-0000-0000-0000-000000000000", value = { mails = filtered }, permission_read = 2, permission_write = 0 }}) else if targetUserId == "" then error("target_user_id required for personal mail") end local pObjs = nk.storage_read({{ collection = "inbox", key = "personal", user_id = targetUserId }}) local personalMails = (#pObjs > 0) and (pObjs[1].value.mails or {}) or {} local before = #personalMails local filtered = {} for _, m in ipairs(personalMails) do if m.id ~= mailId then table.insert(filtered, m) end end if #filtered == before then error("Mail not found") end nk.storage_write({{ collection = "inbox", key = "personal", user_id = targetUserId, value = { mails = filtered }, permission_read = 1, permission_write = 0 }}) end nk.logger_info("Admin deleted mail " .. mailId .. " from server by " .. context.user_id) return nk.json_encode({ success = true }) end nk.register_rpc(inbox.rpc_admin_send_mail, "admin_send_mail") nk.register_rpc(inbox.rpc_get_mail, "get_mail") nk.register_rpc(inbox.rpc_claim_mail_reward, "claim_mail_reward") nk.register_rpc(inbox.rpc_delete_mail, "delete_mail") nk.register_rpc(inbox.rpc_save_mail_state, "save_mail_state") nk.register_rpc(inbox.rpc_admin_list_mail, "admin_list_mail") nk.register_rpc(inbox.rpc_admin_update_mail, "admin_update_mail") nk.register_rpc(inbox.rpc_admin_delete_mail_server, "admin_delete_mail_server") nk.logger_info("LUA TEST: inbox module loaded") return inbox