feat: 2.3.2
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
local nk = require("nakama")
|
||||
local utils = require("lua.utils")
|
||||
|
||||
local admin = {}
|
||||
|
||||
function admin.rpc_admin_kick_player(context, payload)
|
||||
local request = nk.json_decode(payload)
|
||||
utils.require_admin_or_host(context, request.match_id)
|
||||
|
||||
if request.user_id == context.user_id then
|
||||
error("Cannot kick yourself")
|
||||
end
|
||||
|
||||
local status, err = pcall(nk.match_signal, request.match_id, nk.json_encode({
|
||||
action = "kick",
|
||||
user_id = request.user_id,
|
||||
reason = request.reason or "Kicked by admin"
|
||||
}))
|
||||
|
||||
if not status then
|
||||
nk.logger_error("Failed to kick player: " .. tostring(err))
|
||||
error("Failed to kick player")
|
||||
end
|
||||
|
||||
nk.logger_info("Player " .. request.user_id .. " kicked from match " .. request.match_id .. " by " .. context.user_id)
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function admin.rpc_admin_ban_player(context, payload)
|
||||
local request = nk.json_decode(payload)
|
||||
utils.require_admin(context)
|
||||
|
||||
if request.user_id == context.user_id then
|
||||
error("Cannot ban yourself")
|
||||
end
|
||||
|
||||
local status, targetAccount = pcall(nk.account_get_id, request.user_id)
|
||||
if not status then error("Target account not found") end
|
||||
|
||||
local metadata = {}
|
||||
if targetAccount.user.metadata then
|
||||
status, metadata = pcall(nk.json_decode, targetAccount.user.metadata)
|
||||
if not status then metadata = {} end
|
||||
end
|
||||
|
||||
if utils.ADMIN_ROLES[metadata.role or ""] then
|
||||
error("Cannot ban an admin")
|
||||
end
|
||||
|
||||
local banExpires = nil
|
||||
if request.duration_hours and request.duration_hours > 0 then
|
||||
-- Unix time in seconds for lua, Nakama might expect ISO string or unix
|
||||
banExpires = os.time() + (request.duration_hours * 60 * 60)
|
||||
end
|
||||
|
||||
metadata.banned = true
|
||||
metadata.ban_reason = request.reason or "Banned by admin"
|
||||
if banExpires then metadata.ban_expires = banExpires end
|
||||
|
||||
nk.account_update_id(request.user_id, nil, nil, nil, nil, nil, nil, nk.json_encode(metadata))
|
||||
|
||||
if request.match_id then
|
||||
pcall(nk.match_signal, request.match_id, nk.json_encode({
|
||||
action = "kick",
|
||||
user_id = request.user_id,
|
||||
reason = "Banned: " .. (request.reason or "")
|
||||
}))
|
||||
end
|
||||
|
||||
local banRecord = {
|
||||
user_id = request.user_id,
|
||||
username = targetAccount.user.username,
|
||||
banned_by = context.user_id,
|
||||
banned_at = os.time(),
|
||||
reason = request.reason,
|
||||
expires = banExpires
|
||||
}
|
||||
|
||||
nk.storage_write({{
|
||||
collection = "bans",
|
||||
key = request.user_id,
|
||||
user_id = "00000000-0000-0000-0000-000000000000",
|
||||
value = banRecord,
|
||||
permission_read = 2,
|
||||
permission_write = 0
|
||||
}})
|
||||
|
||||
nk.logger_warn("Player " .. request.user_id .. " banned by " .. context.user_id)
|
||||
return nk.json_encode({ success = true, ban = banRecord })
|
||||
end
|
||||
|
||||
function admin.rpc_admin_unban_player(context, payload)
|
||||
local request = nk.json_decode(payload)
|
||||
utils.require_admin(context)
|
||||
|
||||
local status, targetAccount = pcall(nk.account_get_id, request.user_id)
|
||||
if not status then error("Target account not found") end
|
||||
|
||||
local metadata = {}
|
||||
if targetAccount.user.metadata then
|
||||
status, metadata = pcall(nk.json_decode, targetAccount.user.metadata)
|
||||
if not status then metadata = {} end
|
||||
end
|
||||
|
||||
metadata.banned = nil
|
||||
metadata.ban_reason = nil
|
||||
metadata.ban_expires = nil
|
||||
|
||||
nk.account_update_id(request.user_id, nil, nil, nil, nil, nil, nil, nk.json_encode(metadata))
|
||||
|
||||
nk.storage_delete({{
|
||||
collection = "bans",
|
||||
key = request.user_id,
|
||||
user_id = "00000000-0000-0000-0000-000000000000"
|
||||
}})
|
||||
|
||||
nk.logger_info("Player " .. request.user_id .. " unbanned by " .. context.user_id)
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function admin.rpc_admin_get_ban_list(context, payload)
|
||||
utils.require_admin(context)
|
||||
|
||||
local status, result = pcall(nk.storage_list, "00000000-0000-0000-0000-000000000000", "bans", 100)
|
||||
local bans = {}
|
||||
|
||||
if status and result and result.objects then
|
||||
for _, obj in ipairs(result.objects) do
|
||||
table.insert(bans, obj.value)
|
||||
end
|
||||
end
|
||||
|
||||
return nk.json_encode({ bans = bans })
|
||||
end
|
||||
|
||||
function admin.rpc_admin_get_server_stats(context, payload)
|
||||
local request = nk.json_decode(payload or "{}")
|
||||
|
||||
if request.match_id then
|
||||
utils.require_admin_or_host(context, request.match_id)
|
||||
else
|
||||
utils.require_admin(context)
|
||||
end
|
||||
|
||||
local matches = nk.match_list(100, true)
|
||||
local activeMatchCount = matches and #matches or 0
|
||||
|
||||
local totalPlayers = 0
|
||||
if matches then
|
||||
for _, match in ipairs(matches) do
|
||||
totalPlayers = totalPlayers + (match.size or 0)
|
||||
end
|
||||
end
|
||||
|
||||
local stats = {
|
||||
active_matches = activeMatchCount,
|
||||
total_players = totalPlayers,
|
||||
server_time = os.time()
|
||||
}
|
||||
|
||||
if request.match_id then
|
||||
local status, match = pcall(nk.match_get, request.match_id)
|
||||
if status and match then
|
||||
stats.match = {
|
||||
id = match.match_id,
|
||||
size = match.size,
|
||||
tick_rate = match.tick_rate,
|
||||
authoritative = match.authoritative
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
return nk.json_encode(stats)
|
||||
end
|
||||
|
||||
function admin.rpc_admin_end_match(context, payload)
|
||||
local request = nk.json_decode(payload)
|
||||
utils.require_admin_or_host(context, request.match_id)
|
||||
|
||||
nk.match_signal(request.match_id, nk.json_encode({
|
||||
action = "end_match",
|
||||
reason = request.reason or "Ended by admin"
|
||||
}))
|
||||
|
||||
nk.logger_info("Match " .. request.match_id .. " ended by " .. context.user_id)
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function admin.rpc_admin_set_user_role(context, payload)
|
||||
local request = nk.json_decode(payload)
|
||||
|
||||
local callerAccount = nk.account_get_id(context.user_id)
|
||||
local callerMetadata = nk.json_decode(callerAccount.user.metadata or "{}")
|
||||
|
||||
if callerMetadata.role ~= "owner" then
|
||||
error("Only owners can modify user roles")
|
||||
end
|
||||
|
||||
local validRoles = { player = true, moderator = true, admin = true }
|
||||
if not validRoles[request.role] then
|
||||
error("Invalid role")
|
||||
end
|
||||
|
||||
local targetAccount = nk.account_get_id(request.user_id)
|
||||
local metadata = nk.json_decode(targetAccount.user.metadata or "{}")
|
||||
metadata.role = request.role
|
||||
|
||||
nk.account_update_id(request.user_id, nil, nil, nil, nil, nil, nil, nk.json_encode(metadata))
|
||||
|
||||
nk.logger_info("User " .. request.user_id .. " role set to " .. request.role .. " by " .. context.user_id)
|
||||
return nk.json_encode({ success = true, role = request.role })
|
||||
end
|
||||
|
||||
function admin.rpc_admin_topup_gold(context, payload)
|
||||
utils.require_admin(context)
|
||||
nk.wallet_update(context.user_id, { gold = 999999 })
|
||||
nk.logger_info("Admin gold top-up applied for user " .. context.user_id)
|
||||
return nk.json_encode({ success = true, gold_added = 999999 })
|
||||
end
|
||||
|
||||
function admin.rpc_admin_clear_global_chat(context, payload)
|
||||
utils.require_admin(context)
|
||||
|
||||
local request = nk.json_decode(payload or "{}")
|
||||
local channelId = request.channel_id or ""
|
||||
|
||||
if channelId == "" then
|
||||
error("channel_id is required. Pass the channel ID from the client.")
|
||||
end
|
||||
|
||||
local deleted = 0
|
||||
local cursor = ""
|
||||
|
||||
repeat
|
||||
local status, result = pcall(nk.channel_messages_list, channelId, 100, false, cursor)
|
||||
if not status then break end
|
||||
|
||||
local messages = result.messages or {}
|
||||
for _, msg in ipairs(messages) do
|
||||
pcall(nk.channel_message_remove, channelId, msg.message_id)
|
||||
deleted = deleted + 1
|
||||
end
|
||||
|
||||
cursor = result.next_cursor or ""
|
||||
until cursor == ""
|
||||
|
||||
nk.logger_info("[AdminClearGlobalChat] Deleted " .. deleted .. " messages by " .. context.user_id)
|
||||
return nk.json_encode({ success = true, deleted = deleted })
|
||||
end
|
||||
|
||||
function admin.rpc_admin_list_users(context, payload)
|
||||
utils.require_admin(context)
|
||||
|
||||
local users = {}
|
||||
local sql = "SELECT id, username, display_name, metadata, create_time FROM users WHERE id != '00000000-0000-0000-0000-000000000000' ORDER BY create_time DESC LIMIT 500"
|
||||
|
||||
local status, rows = pcall(nk.sql_query, sql, {})
|
||||
if status and rows then
|
||||
for _, row in ipairs(rows) do
|
||||
local metadata = {}
|
||||
if row.metadata then
|
||||
local s, m = pcall(nk.json_decode, row.metadata)
|
||||
if s then metadata = m end
|
||||
end
|
||||
|
||||
table.insert(users, {
|
||||
user_id = row.id,
|
||||
username = row.username or "",
|
||||
display_name = row.display_name or row.username or "",
|
||||
create_time = row.create_time,
|
||||
role = metadata.role or "player",
|
||||
banned = metadata.banned or false,
|
||||
ban_reason = metadata.ban_reason or ""
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return nk.json_encode({ users = users, count = #users })
|
||||
end
|
||||
|
||||
function admin.rpc_admin_delete_users(context, payload)
|
||||
utils.require_admin(context)
|
||||
|
||||
local request = nk.json_decode(payload)
|
||||
local userIds = request.user_ids or {}
|
||||
|
||||
if #userIds == 0 then error("No user IDs provided") end
|
||||
|
||||
for _, uid in ipairs(userIds) do
|
||||
if uid == context.user_id then
|
||||
error("Cannot delete your own account")
|
||||
end
|
||||
end
|
||||
|
||||
local deleted = {}
|
||||
local failed = {}
|
||||
|
||||
for _, uid in ipairs(userIds) do
|
||||
local status, err = pcall(function()
|
||||
local account = nk.account_get_id(uid)
|
||||
local meta = {}
|
||||
if account.user.metadata then
|
||||
local s, m = pcall(nk.json_decode, account.user.metadata)
|
||||
if s then meta = m end
|
||||
end
|
||||
if meta.role == "admin" or meta.role == "moderator" or meta.role == "owner" then
|
||||
error("Cannot delete admin account")
|
||||
end
|
||||
nk.account_delete_id(uid, false)
|
||||
table.insert(deleted, uid)
|
||||
nk.logger_warn("User " .. uid .. " deleted by " .. context.user_id)
|
||||
end)
|
||||
|
||||
if not status then
|
||||
table.insert(failed, { user_id = uid, reason = tostring(err) })
|
||||
end
|
||||
end
|
||||
|
||||
return nk.json_encode({ success = true, deleted = deleted, failed = failed })
|
||||
end
|
||||
|
||||
function admin.rpc_admin_get_player_list(context, payload)
|
||||
local request = nk.json_decode(payload)
|
||||
utils.require_admin_or_host(context, request.match_id)
|
||||
|
||||
local status, match = pcall(nk.match_get, request.match_id)
|
||||
if not status or not match then
|
||||
error("Match not found")
|
||||
end
|
||||
|
||||
-- Get player details
|
||||
-- Note: In actual implementation, you'd need to track presences
|
||||
-- This is a simplified version - adjust based on your match handler
|
||||
local players = {}
|
||||
|
||||
return nk.json_encode({ players = players })
|
||||
end
|
||||
|
||||
-- Register RPCs
|
||||
nk.register_rpc(admin.rpc_admin_kick_player, "admin_kick_player")
|
||||
nk.register_rpc(admin.rpc_admin_ban_player, "admin_ban_player")
|
||||
nk.register_rpc(admin.rpc_admin_unban_player, "admin_unban_player")
|
||||
nk.register_rpc(admin.rpc_admin_get_ban_list, "admin_get_ban_list")
|
||||
nk.register_rpc(admin.rpc_admin_get_server_stats, "admin_get_server_stats")
|
||||
nk.register_rpc(admin.rpc_admin_get_player_list, "admin_get_player_list")
|
||||
nk.register_rpc(admin.rpc_admin_end_match, "admin_end_match")
|
||||
nk.register_rpc(admin.rpc_admin_set_user_role, "admin_set_user_role")
|
||||
nk.register_rpc(admin.rpc_admin_topup_gold, "admin_topup_gold")
|
||||
nk.register_rpc(admin.rpc_admin_clear_global_chat, "admin_clear_global_chat")
|
||||
nk.register_rpc(admin.rpc_admin_list_users, "admin_list_users")
|
||||
nk.register_rpc(admin.rpc_admin_delete_users, "admin_delete_users")
|
||||
|
||||
nk.logger_info("LUA TEST: admin module loaded successfully")
|
||||
|
||||
return admin
|
||||
@@ -0,0 +1,42 @@
|
||||
local nk = require("nakama")
|
||||
|
||||
-- =============================================================================
|
||||
-- Steam Auth Hook
|
||||
-- =============================================================================
|
||||
-- On first Steam login: set display_name from Steam username, default role to "player"
|
||||
|
||||
local function after_authenticate_steam(context, output, input)
|
||||
if not context.user_id then return end
|
||||
|
||||
local status, account = pcall(nk.account_get_id, context.user_id)
|
||||
if not status or not account then return end
|
||||
|
||||
-- On first login (no display name set), use Steam username
|
||||
if not account.user.display_name or account.user.display_name == "" then
|
||||
local steamName = input.username or "SteamPlayer"
|
||||
pcall(nk.account_update_id, context.user_id, nil, steamName, nil, nil, nil, nil, nil)
|
||||
nk.logger_info("Steam user " .. context.user_id .. " display name set to: " .. steamName)
|
||||
end
|
||||
|
||||
-- Set default role if not set
|
||||
local metadata = {}
|
||||
if type(account.user.metadata) == "string" then
|
||||
local s, m = pcall(nk.json_decode, account.user.metadata or "{}")
|
||||
if s then metadata = m end
|
||||
else
|
||||
metadata = account.user.metadata or {}
|
||||
end
|
||||
|
||||
if not metadata.role or metadata.role == "" then
|
||||
metadata.role = "player"
|
||||
pcall(nk.account_update_id, context.user_id, nil, nil, nil, nil, nil, nil, nk.json_encode(metadata))
|
||||
end
|
||||
end
|
||||
|
||||
-- Register the Steam auth after-hook
|
||||
nk.register_req_after(after_authenticate_steam, "AuthenticateSteam")
|
||||
|
||||
-- Create default native leaderboard on startup
|
||||
pcall(nk.leaderboard_create, "global_high_score", true, "desc", "best", nil, {})
|
||||
|
||||
nk.logger_info("LUA TEST: core module loaded successfully")
|
||||
@@ -0,0 +1,205 @@
|
||||
local nk = require("nakama")
|
||||
local utils = require("lua.utils")
|
||||
|
||||
local daily_rewards = {}
|
||||
|
||||
function daily_rewards.rpc_claim_daily_reward(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
|
||||
local now = os.date("!*t")
|
||||
local currentMonth = string.format("%02d", now.month)
|
||||
local todayStr = string.format("%04d-%02d-%02d", now.year, now.month, now.day)
|
||||
local todayIndex = now.day - 1 -- 0 to 30
|
||||
|
||||
local stateObjs = nk.storage_read({{ collection = "daily_rewards", key = "state", user_id = context.user_id }})
|
||||
local state = { claimed_days = {}, last_claim_date = "", month = "" }
|
||||
|
||||
if stateObjs and #stateObjs > 0 then
|
||||
local val = stateObjs[1].value
|
||||
state.last_claim_date = val.last_claim_date or ""
|
||||
state.month = val.month or ""
|
||||
|
||||
if type(val.claimed_days) == "number" then
|
||||
for i = 0, val.claimed_days - 1 do
|
||||
table.insert(state.claimed_days, i)
|
||||
end
|
||||
elseif type(val.claimed_days) == "table" then
|
||||
state.claimed_days = val.claimed_days
|
||||
end
|
||||
end
|
||||
|
||||
if state.month ~= currentMonth then
|
||||
state.claimed_days = {}
|
||||
state.month = currentMonth
|
||||
end
|
||||
|
||||
if state.last_claim_date == todayStr then
|
||||
error("Already claimed today")
|
||||
end
|
||||
|
||||
local configObjs = nk.storage_read({{ collection = "config", key = "daily_rewards", user_id = "00000000-0000-0000-0000-000000000000" }})
|
||||
local config = {}
|
||||
if configObjs and #configObjs > 0 then
|
||||
config = configObjs[1].value
|
||||
end
|
||||
|
||||
local monthRewards = config[currentMonth]
|
||||
if not monthRewards or #monthRewards == 0 then
|
||||
monthRewards = {}
|
||||
for i = 0, 30 do
|
||||
table.insert(monthRewards, { type = "star", amount = math.min(10 + i * 5, 100) })
|
||||
end
|
||||
end
|
||||
|
||||
local dayIndex = todayIndex
|
||||
-- In lua array size is #monthRewards
|
||||
if dayIndex >= #monthRewards then
|
||||
error("Already claimed all rewards for this month")
|
||||
end
|
||||
|
||||
local hasClaimed = false
|
||||
for _, claimed_day in ipairs(state.claimed_days) do
|
||||
if claimed_day == dayIndex then
|
||||
hasClaimed = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if hasClaimed then
|
||||
error("Already claimed today's reward")
|
||||
end
|
||||
|
||||
-- Lua arrays are 1-indexed!
|
||||
local rewardData = monthRewards[dayIndex + 1]
|
||||
if type(rewardData) == "number" then
|
||||
rewardData = { type = "star", amount = rewardData }
|
||||
end
|
||||
|
||||
local rewardType = rewardData.type or "star"
|
||||
local rewardAmount = rewardData.amount or 0
|
||||
|
||||
if rewardType == "star" or rewardType == "gold" then
|
||||
local changes = {}
|
||||
changes[rewardType] = rewardAmount
|
||||
nk.wallet_update(context.user_id, changes, {}, true)
|
||||
elseif string.sub(rewardType, 1, 5) == "frag_" then
|
||||
local invObjs = nk.storage_read({{ collection = "inventory", key = "fragments", user_id = context.user_id }})
|
||||
local frags = {}
|
||||
if invObjs and #invObjs > 0 then
|
||||
frags = invObjs[1].value
|
||||
end
|
||||
frags[rewardType] = (frags[rewardType] or 0) + rewardAmount
|
||||
nk.storage_write({{
|
||||
collection = "inventory",
|
||||
key = "fragments",
|
||||
user_id = context.user_id,
|
||||
value = frags,
|
||||
permission_read = 1,
|
||||
permission_write = 0
|
||||
}})
|
||||
end
|
||||
|
||||
table.insert(state.claimed_days, dayIndex)
|
||||
state.last_claim_date = todayStr
|
||||
|
||||
nk.storage_write({{
|
||||
collection = "daily_rewards",
|
||||
key = "state",
|
||||
user_id = context.user_id,
|
||||
value = state,
|
||||
permission_read = 1,
|
||||
permission_write = 0
|
||||
}})
|
||||
|
||||
return nk.json_encode({ success = true, reward_type = rewardType, reward_amount = rewardAmount, day = dayIndex + 1 })
|
||||
end
|
||||
|
||||
function daily_rewards.rpc_get_daily_reward_state(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
|
||||
local now = os.date("!*t")
|
||||
local currentMonth = string.format("%02d", now.month)
|
||||
local todayStr = string.format("%04d-%02d-%02d", now.year, now.month, now.day)
|
||||
local todayIndex = now.day - 1
|
||||
|
||||
local stateObjs = nk.storage_read({{ collection = "daily_rewards", key = "state", user_id = context.user_id }})
|
||||
local state = { claimed_days = {}, last_claim_date = "", month = "" }
|
||||
if stateObjs and #stateObjs > 0 then
|
||||
local val = stateObjs[1].value
|
||||
state.last_claim_date = val.last_claim_date or ""
|
||||
state.month = val.month or ""
|
||||
if type(val.claimed_days) == "number" then
|
||||
for i = 0, val.claimed_days - 1 do
|
||||
table.insert(state.claimed_days, i)
|
||||
end
|
||||
elseif type(val.claimed_days) == "table" then
|
||||
state.claimed_days = val.claimed_days
|
||||
end
|
||||
end
|
||||
if state.month ~= currentMonth then
|
||||
state.claimed_days = {}
|
||||
state.month = currentMonth
|
||||
end
|
||||
|
||||
local configObjs = nk.storage_read({{ collection = "config", key = "daily_rewards", user_id = "00000000-0000-0000-0000-000000000000" }})
|
||||
local config = {}
|
||||
if configObjs and #configObjs > 0 then
|
||||
config = configObjs[1].value
|
||||
end
|
||||
local monthRewards = config[currentMonth]
|
||||
if not monthRewards or #monthRewards == 0 then
|
||||
monthRewards = {}
|
||||
for i = 0, 30 do
|
||||
table.insert(monthRewards, { type = "star", amount = math.min(10 + i * 5, 100) })
|
||||
end
|
||||
end
|
||||
|
||||
local hasClaimedToday = false
|
||||
for _, claimed_day in ipairs(state.claimed_days) do
|
||||
if claimed_day == todayIndex then hasClaimedToday = true break end
|
||||
end
|
||||
|
||||
local canClaimToday = (state.last_claim_date ~= todayStr) and (not hasClaimedToday) and (todayIndex < #monthRewards)
|
||||
|
||||
return nk.json_encode({
|
||||
state = state,
|
||||
month_rewards = monthRewards,
|
||||
can_claim_today = canClaimToday,
|
||||
today_date = todayStr,
|
||||
today_index = todayIndex,
|
||||
server_month = now.month
|
||||
})
|
||||
end
|
||||
|
||||
function daily_rewards.rpc_set_daily_reward_config(context, payload)
|
||||
utils.require_admin(context)
|
||||
local request = nk.json_decode(payload or "{}")
|
||||
nk.storage_write({{
|
||||
collection = "config",
|
||||
key = "daily_rewards",
|
||||
user_id = "00000000-0000-0000-0000-000000000000",
|
||||
value = request.config,
|
||||
permission_read = 2,
|
||||
permission_write = 0
|
||||
}})
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function daily_rewards.rpc_get_daily_reward_config_admin(context, payload)
|
||||
utils.require_admin(context)
|
||||
local configObjs = nk.storage_read({{ collection = "config", key = "daily_rewards", user_id = "00000000-0000-0000-0000-000000000000" }})
|
||||
local config = {}
|
||||
if configObjs and #configObjs > 0 then
|
||||
config = configObjs[1].value
|
||||
end
|
||||
return nk.json_encode({ config = config })
|
||||
end
|
||||
|
||||
nk.register_rpc(daily_rewards.rpc_claim_daily_reward, "claim_daily_reward")
|
||||
nk.register_rpc(daily_rewards.rpc_get_daily_reward_state, "get_daily_reward_state")
|
||||
nk.register_rpc(daily_rewards.rpc_set_daily_reward_config, "set_daily_reward_config")
|
||||
nk.register_rpc(daily_rewards.rpc_get_daily_reward_config_admin, "get_daily_reward_config_admin")
|
||||
|
||||
nk.logger_info("LUA TEST: daily rewards module loaded")
|
||||
|
||||
return daily_rewards
|
||||
@@ -0,0 +1,244 @@
|
||||
local nk = require("nakama")
|
||||
local utils = require("lua.utils")
|
||||
|
||||
local economy = {}
|
||||
|
||||
local SHOP_CATALOG_DEFS = {
|
||||
{ id = "oldpop-blue-hat", name = "Oldpop Blue Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" },
|
||||
{ id = "oldpop-green-hat", name = "Oldpop Green Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" },
|
||||
{ id = "oldpop-red-hat", name = "Oldpop Red Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" },
|
||||
{ id = "oldpop-yellow-hat", name = "Oldpop Yellow Hat", category = "head", gold = 100, star = 0, rarity = "Common", character = "Oldpop" },
|
||||
{ id = "oldpop-og-pant", name = "Copper OG Pant", category = "costume", gold = 0, star = 0, rarity = "Common", character = "Oldpop" },
|
||||
{ id = "oldpop-grey-pant", name = "Copper Grey Pant", category = "costume", gold = 150, star = 0, rarity = "Common", character = "Oldpop" },
|
||||
{ id = "oldpop-red-pant", name = "Copper Red Pant", category = "costume", gold = 150, star = 0, rarity = "Common", character = "Oldpop" },
|
||||
{ id = "oldpop-yellow-pant", name = "Copper Yellow Pant", category = "costume", gold = 150, star = 0, rarity = "Common", character = "Oldpop" },
|
||||
{ id = "oldpop-blue-gloves", name = "Oldpop Blue Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" },
|
||||
{ id = "oldpop-green-gloves", name = "Oldpop Green Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" },
|
||||
{ id = "oldpop-red-gloves", name = "Oldpop Red Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" },
|
||||
{ id = "oldpop-yellow-gloves", name = "Oldpop Yellow Gloves", category = "glove", gold = 75, star = 0, rarity = "Common", character = "Oldpop" }
|
||||
}
|
||||
|
||||
local function build_shop_catalog()
|
||||
local catalog = {}
|
||||
for _, def in ipairs(SHOP_CATALOG_DEFS) do
|
||||
local cat = def.category
|
||||
if not catalog[cat] then catalog[cat] = {} end
|
||||
local entry = {
|
||||
id = def.id,
|
||||
name = def.name,
|
||||
gold = def.gold or 0,
|
||||
star = def.star or 0,
|
||||
rarity = def.rarity or "Common",
|
||||
character = def.character
|
||||
}
|
||||
table.insert(catalog[cat], entry)
|
||||
end
|
||||
return catalog
|
||||
end
|
||||
|
||||
function economy.rpc_get_shop_catalog(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
|
||||
local result = { catalog = build_shop_catalog(), featured_banners = {} }
|
||||
|
||||
local status, objs = pcall(nk.storage_read, {{ collection = "shop_config", key = "featured_banners", user_id = "00000000-0000-0000-0000-000000000000" }})
|
||||
if status and objs and #objs > 0 then
|
||||
local val = objs[1].value
|
||||
if val.banners then result.featured_banners = val.banners end
|
||||
end
|
||||
|
||||
return nk.json_encode(result)
|
||||
end
|
||||
|
||||
function economy.rpc_buy_currency(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
|
||||
local request = nk.json_decode(payload)
|
||||
local packageId = request.package_id
|
||||
local receipt = request.receipt
|
||||
local idempotencyKey = request.idempotency_key
|
||||
|
||||
if not packageId or packageId == "" then error("Package ID required") end
|
||||
if not idempotencyKey or idempotencyKey == "" then error("Idempotency key required") end
|
||||
|
||||
local status, existing = pcall(nk.storage_read, {{ collection = "receipts", key = idempotencyKey, user_id = context.user_id }})
|
||||
if status and existing and #existing > 0 then
|
||||
return nk.json_encode({ success = true, package_id = packageId, duplicate = true, status = existing[1].value.status })
|
||||
end
|
||||
|
||||
local changeset = { gold = 0, star = 0 }
|
||||
local requiresVerification = false
|
||||
|
||||
if packageId == "gold_100" then changeset.gold = 100; requiresVerification = true
|
||||
elseif packageId == "gold_500" then changeset.gold = 550; requiresVerification = true
|
||||
elseif packageId == "gold_1000" then changeset.gold = 1150; requiresVerification = true
|
||||
elseif packageId == "gold_2000" then changeset.gold = 2400; requiresVerification = true
|
||||
elseif packageId == "gold_5000" then changeset.gold = 6250; requiresVerification = true
|
||||
elseif packageId == "gold_10000" then changeset.gold = 13000; requiresVerification = true
|
||||
elseif packageId == "star_100" then changeset.star = 100; changeset.gold = -500
|
||||
elseif packageId == "star_250" then changeset.star = 250; changeset.gold = -1100
|
||||
elseif packageId == "star_600" then changeset.star = 600; changeset.gold = -2500
|
||||
else error("Invalid package ID") end
|
||||
|
||||
if requiresVerification and not receipt then
|
||||
nk.storage_write({{
|
||||
collection = "receipts",
|
||||
key = idempotencyKey,
|
||||
user_id = context.user_id,
|
||||
value = { type = "currency", package_id = packageId, status = "pending", created_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") },
|
||||
permission_read = 1,
|
||||
permission_write = 0
|
||||
}})
|
||||
return nk.json_encode({ success = true, status = "pending", package_id = packageId })
|
||||
end
|
||||
|
||||
local s, err = pcall(function()
|
||||
if changeset.gold ~= 0 or changeset.star ~= 0 then
|
||||
nk.wallet_update(context.user_id, changeset, {}, true)
|
||||
end
|
||||
nk.storage_write({{
|
||||
collection = "receipts",
|
||||
key = idempotencyKey,
|
||||
user_id = context.user_id,
|
||||
value = { type = "currency", package_id = packageId, changeset = changeset, receipt = receipt or nk.json_null(), status = "verified", processed_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") },
|
||||
permission_read = 1,
|
||||
permission_write = 0
|
||||
}})
|
||||
end)
|
||||
|
||||
if not s then
|
||||
nk.logger_error("Currency purchase failed: " .. tostring(err))
|
||||
error("NotEnoughFunds")
|
||||
end
|
||||
|
||||
nk.logger_info("User " .. context.user_id .. " bought currency package " .. packageId)
|
||||
return nk.json_encode({ success = true, status = "verified", package_id = packageId })
|
||||
end
|
||||
|
||||
function economy.rpc_purchase_item(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
|
||||
local request = nk.json_decode(payload)
|
||||
local itemId = request.item_id
|
||||
local quantity = request.quantity or 1
|
||||
local idempotencyKey = request.idempotency_key
|
||||
|
||||
if not itemId or itemId == "" then error("Item ID required") end
|
||||
if quantity < 1 then error("Invalid quantity") end
|
||||
if not idempotencyKey or idempotencyKey == "" then error("Idempotency key required") end
|
||||
|
||||
local status, existing = pcall(nk.storage_read, {{ collection = "receipts", key = idempotencyKey, user_id = context.user_id }})
|
||||
if status and existing and #existing > 0 then
|
||||
return nk.json_encode({ success = true, item = itemId, duplicate = true })
|
||||
end
|
||||
|
||||
local itemDef = nil
|
||||
for _, def in ipairs(SHOP_CATALOG_DEFS) do
|
||||
if def.id == itemId then
|
||||
itemDef = def
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not itemDef then error("ItemNotFound") end
|
||||
|
||||
local priceGold = (itemDef.gold or 0) * quantity
|
||||
local priceStar = (itemDef.star or 0) * quantity
|
||||
local category = itemDef.category or "accessory"
|
||||
|
||||
local s, err = pcall(function()
|
||||
local changeset = {}
|
||||
if priceGold > 0 then changeset.gold = -priceGold end
|
||||
if priceStar > 0 then changeset.star = -priceStar end
|
||||
|
||||
if priceGold > 0 or priceStar > 0 then
|
||||
nk.wallet_update(context.user_id, changeset, {}, true)
|
||||
end
|
||||
end)
|
||||
if not s then
|
||||
nk.logger_error("Wallet update failed: " .. tostring(err))
|
||||
error("NotEnoughFunds")
|
||||
end
|
||||
|
||||
local s2, err2 = pcall(function()
|
||||
local writes = {
|
||||
{
|
||||
collection = "inventory",
|
||||
key = itemId,
|
||||
user_id = context.user_id,
|
||||
value = { category = category, purchased_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z"), quantity = quantity },
|
||||
permission_read = 1,
|
||||
permission_write = 0
|
||||
},
|
||||
{
|
||||
collection = "receipts",
|
||||
key = idempotencyKey,
|
||||
user_id = context.user_id,
|
||||
value = { type = "item", item_id = itemId, quantity = quantity, cost = { gold = priceGold, star = priceStar }, processed_at = os.date("!%Y-%m-%dT%H:%M:%S.000Z") },
|
||||
permission_read = 1,
|
||||
permission_write = 0
|
||||
}
|
||||
}
|
||||
nk.storage_write(writes)
|
||||
end)
|
||||
if not s2 then
|
||||
nk.logger_error("Purchase failed: " .. tostring(err2))
|
||||
error("PurchaseFailed")
|
||||
end
|
||||
|
||||
nk.logger_info("User " .. context.user_id .. " purchased " .. itemId)
|
||||
return nk.json_encode({ success = true, item = itemId })
|
||||
end
|
||||
|
||||
function economy.rpc_admin_set_featured_banners(context, payload)
|
||||
utils.require_admin(context)
|
||||
local req = nk.json_decode(payload or "{}")
|
||||
local banners = req.banners or {}
|
||||
|
||||
local finalBanners = {}
|
||||
for i = 1, math.min(#banners, 3) do
|
||||
table.insert(finalBanners, banners[i])
|
||||
end
|
||||
|
||||
for _, b in ipairs(finalBanners) do
|
||||
local itemId = b.item_id or ""
|
||||
if itemId ~= "" then
|
||||
local found = false
|
||||
for _, def in ipairs(SHOP_CATALOG_DEFS) do
|
||||
if def.id == itemId then found = true; break end
|
||||
end
|
||||
if not found then error("Item not found in catalog: " .. itemId) end
|
||||
end
|
||||
end
|
||||
|
||||
nk.storage_write({{
|
||||
collection = "shop_config",
|
||||
key = "featured_banners",
|
||||
user_id = "00000000-0000-0000-0000-000000000000",
|
||||
value = { banners = finalBanners },
|
||||
permission_read = 2,
|
||||
permission_write = 0
|
||||
}})
|
||||
|
||||
nk.logger_info("Featured banners updated by admin " .. context.user_id)
|
||||
return nk.json_encode({ success = true, banners = finalBanners })
|
||||
end
|
||||
|
||||
function economy.rpc_admin_get_featured_banners(context, payload)
|
||||
utils.require_admin(context)
|
||||
local status, objs = pcall(nk.storage_read, {{ collection = "shop_config", key = "featured_banners", user_id = "00000000-0000-0000-0000-000000000000" }})
|
||||
if status and objs and #objs > 0 then
|
||||
return nk.json_encode({ banners = objs[1].value.banners or {} })
|
||||
end
|
||||
return nk.json_encode({ banners = {} })
|
||||
end
|
||||
|
||||
nk.register_rpc(economy.rpc_get_shop_catalog, "get_shop_catalog")
|
||||
nk.register_rpc(economy.rpc_buy_currency, "buy_currency")
|
||||
nk.register_rpc(economy.rpc_purchase_item, "purchase_item")
|
||||
nk.register_rpc(economy.rpc_admin_set_featured_banners, "admin_set_featured_banners")
|
||||
nk.register_rpc(economy.rpc_admin_get_featured_banners, "admin_get_featured_banners")
|
||||
|
||||
nk.logger_info("LUA TEST: economy module loaded successfully")
|
||||
|
||||
return economy
|
||||
@@ -0,0 +1,535 @@
|
||||
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
|
||||
@@ -0,0 +1,249 @@
|
||||
local nk = require("nakama")
|
||||
local utils = require("lua.utils")
|
||||
|
||||
local leaderboard = {}
|
||||
|
||||
function leaderboard.rpc_get_leaderboard_stats(context, payload)
|
||||
local status, records_or_err = pcall(nk.leaderboard_records_list, "global_high_score", nil, 50, nil)
|
||||
|
||||
if not status then
|
||||
nk.logger_error("Failed to get native leaderboard stats: " .. tostring(records_or_err))
|
||||
return nk.json_encode({ leaderboard = {} })
|
||||
end
|
||||
|
||||
local leaderboardData = {}
|
||||
local ownerRecords = records_or_err.records or {}
|
||||
|
||||
for _, record in ipairs(ownerRecords) do
|
||||
local metadata = {}
|
||||
if record.metadata then
|
||||
local s, m = pcall(nk.json_decode, record.metadata)
|
||||
if s then metadata = m end
|
||||
end
|
||||
|
||||
table.insert(leaderboardData, {
|
||||
user_id = record.owner_id,
|
||||
username = record.username,
|
||||
display_name = record.username, -- Native lua leaderboard returns owner_id, username, score, subscore, num_score, max_num_score, metadata, create_time, update_time
|
||||
avatar_url = metadata.avatar_url or "",
|
||||
loadout_character = metadata.loadout_character or "Copper",
|
||||
high_score = record.score or 0,
|
||||
games_played = metadata.games_played or 0,
|
||||
games_won = metadata.games_won or 0
|
||||
})
|
||||
end
|
||||
|
||||
return nk.json_encode({ leaderboard = leaderboardData })
|
||||
end
|
||||
|
||||
function leaderboard.rpc_submit_score(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
|
||||
local request = nk.json_decode(payload or "{}")
|
||||
local score = tonumber(request.score) or 0
|
||||
local account = nk.account_get_id(context.user_id)
|
||||
|
||||
local metadata = {
|
||||
games_played = request.games_played or 0,
|
||||
games_won = request.games_won or 0,
|
||||
avatar_url = request.avatar_url or account.user.avatar_url or "",
|
||||
loadout_character = request.loadout_character or "Copper"
|
||||
}
|
||||
|
||||
local status, err = pcall(nk.leaderboard_record_write,
|
||||
"global_high_score",
|
||||
context.user_id,
|
||||
account.user.username,
|
||||
score,
|
||||
0,
|
||||
metadata
|
||||
)
|
||||
|
||||
if not status then
|
||||
nk.logger_error("Failed to submit score for " .. context.user_id .. ": " .. tostring(err))
|
||||
error("Failed to submit score")
|
||||
end
|
||||
|
||||
nk.logger_info("Score submitted for user " .. context.user_id .. ": " .. score)
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function leaderboard.rpc_sync_leaderboard(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
|
||||
local status, result = pcall(nk.storage_list, nil, "stats", 100, "")
|
||||
if not status then error("Sync failed: " .. tostring(result)) end
|
||||
|
||||
local statsObjects = result.objects or {}
|
||||
local userGroup = {}
|
||||
|
||||
for _, obj in ipairs(statsObjects) do
|
||||
local userId = obj.user_id
|
||||
local value = obj.value
|
||||
|
||||
if not userGroup[userId] then
|
||||
userGroup[userId] = {
|
||||
high_score = value.high_score or 0,
|
||||
games_played = value.games_played or 0,
|
||||
games_won = value.games_won or 0,
|
||||
avatar_url = value.avatar_url or "",
|
||||
loadout_character = value.loadout_character or ""
|
||||
}
|
||||
else
|
||||
userGroup[userId].high_score = math.max(userGroup[userId].high_score, value.high_score or 0)
|
||||
userGroup[userId].games_played = math.max(userGroup[userId].games_played, value.games_played or 0)
|
||||
userGroup[userId].games_won = math.max(userGroup[userId].games_won, value.games_won or 0)
|
||||
end
|
||||
|
||||
if obj.key == "game_stats" or userGroup[userId].avatar_url == "" then
|
||||
if value.avatar_url then userGroup[userId].avatar_url = value.avatar_url end
|
||||
if value.loadout_character then userGroup[userId].loadout_character = value.loadout_character end
|
||||
end
|
||||
end
|
||||
|
||||
local statusProf, profileResult = pcall(nk.storage_list, nil, "profiles", 100, "")
|
||||
if statusProf and profileResult and profileResult.objects then
|
||||
for _, obj in ipairs(profileResult.objects) do
|
||||
if obj.key == "profile" then
|
||||
local userId = obj.user_id
|
||||
local value = obj.value
|
||||
|
||||
if not userGroup[userId] then
|
||||
userGroup[userId] = { high_score = 0, games_played = 0, games_won = 0, avatar_url = "", loadout_character = "" }
|
||||
end
|
||||
|
||||
if value.avatar_url and userGroup[userId].avatar_url == "" then
|
||||
userGroup[userId].avatar_url = value.avatar_url
|
||||
end
|
||||
if value.loadout_character and userGroup[userId].loadout_character == "" then
|
||||
userGroup[userId].loadout_character = value.loadout_character
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local count = 0
|
||||
local debugLogs = {}
|
||||
|
||||
for uid, stats in pairs(userGroup) do
|
||||
local s, err = pcall(function()
|
||||
local account = nk.account_get_id(uid)
|
||||
local avatar = stats.avatar_url
|
||||
if not avatar or avatar == "" then
|
||||
avatar = account.user.avatar_url
|
||||
end
|
||||
if not avatar or avatar == "" then
|
||||
avatar = "res://assets/graphics/character_selection/sc_characters/sc_copper.png"
|
||||
end
|
||||
|
||||
local meta = {
|
||||
games_played = stats.games_played or 0,
|
||||
games_won = stats.games_won or 0,
|
||||
avatar_url = avatar,
|
||||
loadout_character = stats.loadout_character or "Copper"
|
||||
}
|
||||
nk.leaderboard_record_write("global_high_score", uid, account.user.username, stats.high_score, 0, meta)
|
||||
count = count + 1
|
||||
end)
|
||||
if not s then
|
||||
table.insert(debugLogs, "Error user " .. uid .. ": " .. tostring(err))
|
||||
nk.logger_error("Failed to sync record for " .. uid .. ": " .. tostring(err))
|
||||
end
|
||||
end
|
||||
|
||||
nk.logger_info("Synced " .. count .. " records to leaderboard by user " .. context.user_id)
|
||||
return nk.json_encode({ success = true, synced = count, objects_found = #statsObjects, debug = debugLogs })
|
||||
end
|
||||
|
||||
function leaderboard.rpc_reset_stats(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
|
||||
pcall(nk.leaderboard_record_delete, "global_high_score", context.user_id)
|
||||
|
||||
local zeros = { games_played = 0, games_won = 0, high_score = 0, total_kills = 0, total_deaths = 0 }
|
||||
nk.storage_write({{
|
||||
collection = "stats",
|
||||
key = "game_stats",
|
||||
user_id = context.user_id,
|
||||
value = zeros,
|
||||
permission_read = 2,
|
||||
permission_write = 1
|
||||
}})
|
||||
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function leaderboard.rpc_admin_update_stats(context, payload)
|
||||
utils.require_admin(context)
|
||||
|
||||
local request = nk.json_decode(payload)
|
||||
local targetUserId = request.user_id
|
||||
local stats = request.stats
|
||||
|
||||
if not targetUserId or not stats then
|
||||
error("User ID and stats are required")
|
||||
end
|
||||
|
||||
nk.storage_write({{
|
||||
collection = "stats",
|
||||
key = "game_stats",
|
||||
user_id = targetUserId,
|
||||
value = stats,
|
||||
permission_read = 1,
|
||||
permission_write = 0
|
||||
}})
|
||||
|
||||
local account = nk.account_get_id(targetUserId)
|
||||
local score = stats.high_score or 0
|
||||
local metadata = {
|
||||
games_played = stats.games_played or 0,
|
||||
games_won = stats.games_won or 0,
|
||||
avatar_url = account.user.avatar_url or "",
|
||||
loadout_character = stats.loadout_character or "Copper"
|
||||
}
|
||||
|
||||
nk.leaderboard_record_write("global_high_score", targetUserId, account.user.username, score, 0, metadata)
|
||||
|
||||
nk.logger_info("Stats updated for user " .. targetUserId .. " by admin " .. context.user_id)
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function leaderboard.rpc_admin_delete_stats(context, payload)
|
||||
utils.require_admin(context)
|
||||
|
||||
local request = nk.json_decode(payload)
|
||||
local targetUserId = request.user_id
|
||||
|
||||
if not targetUserId then error("User ID is required") end
|
||||
|
||||
nk.storage_delete({
|
||||
{ collection = "stats", key = "stats", user_id = targetUserId },
|
||||
{ collection = "stats", key = "game_stats", user_id = targetUserId }
|
||||
})
|
||||
|
||||
pcall(nk.leaderboard_record_delete, "global_high_score", targetUserId)
|
||||
|
||||
nk.logger_info("Stats deleted for user " .. targetUserId .. " by admin " .. context.user_id)
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function leaderboard.rpc_admin_sync_leaderboard(context, payload)
|
||||
utils.require_admin(context)
|
||||
return leaderboard.rpc_sync_leaderboard(context, payload)
|
||||
end
|
||||
|
||||
nk.register_rpc(leaderboard.rpc_get_leaderboard_stats, "get_leaderboard_stats")
|
||||
nk.register_rpc(leaderboard.rpc_submit_score, "submit_score")
|
||||
nk.register_rpc(leaderboard.rpc_sync_leaderboard, "sync_leaderboard")
|
||||
nk.register_rpc(leaderboard.rpc_reset_stats, "reset_stats")
|
||||
nk.register_rpc(leaderboard.rpc_admin_update_stats, "admin_update_stats")
|
||||
nk.register_rpc(leaderboard.rpc_admin_delete_stats, "admin_delete_stats")
|
||||
nk.register_rpc(leaderboard.rpc_admin_sync_leaderboard, "admin_sync_leaderboard")
|
||||
|
||||
-- Create default native leaderboard
|
||||
-- id: "global_high_score", authoritative: true, sort: "desc", operator: "best", reset: None
|
||||
pcall(nk.leaderboard_create, "global_high_score", true, "desc", "best", nil, {})
|
||||
|
||||
nk.logger_info("LUA TEST: leaderboard module loaded")
|
||||
|
||||
return leaderboard
|
||||
@@ -0,0 +1,260 @@
|
||||
local nk = require("nakama")
|
||||
|
||||
local user = {}
|
||||
|
||||
function user.rpc_get_user_profile(context, payload)
|
||||
local request = nk.json_decode(payload or "{}")
|
||||
local targetUserId = request.user_id or context.user_id
|
||||
|
||||
local status, account = pcall(nk.account_get_id, targetUserId)
|
||||
if not status then error("Account not found") end
|
||||
|
||||
local metadata = {}
|
||||
if account.user.metadata then
|
||||
status, metadata = pcall(nk.json_decode, account.user.metadata)
|
||||
if not status then metadata = {} end
|
||||
end
|
||||
|
||||
if metadata.banned and targetUserId == context.user_id then
|
||||
if metadata.ban_expires then
|
||||
-- Note: ban_expires stored as Unix time in Lua (seconds) or ISO string depending on how it was stored
|
||||
-- Let's check against current os.time() assuming Unix time
|
||||
local expiresAt = tonumber(metadata.ban_expires)
|
||||
if not expiresAt and type(metadata.ban_expires) == "string" then
|
||||
-- basic check if we stored iso string
|
||||
-- We assume it's valid ISO string and lua os.time might not parse it easily without custom function
|
||||
-- As a fallback, we'll keep the ban if we can't parse it
|
||||
error("Account banned until " .. metadata.ban_expires .. ". Reason: " .. (metadata.ban_reason or ""))
|
||||
end
|
||||
|
||||
if expiresAt and expiresAt <= os.time() then
|
||||
metadata.banned = nil
|
||||
metadata.ban_reason = nil
|
||||
metadata.ban_expires = nil
|
||||
nk.account_update_id(targetUserId, nil, nil, nil, nil, nil, nil, nk.json_encode(metadata))
|
||||
else
|
||||
error("Account banned until " .. tostring(metadata.ban_expires) .. ". Reason: " .. (metadata.ban_reason or ""))
|
||||
end
|
||||
else
|
||||
error("Account permanently banned. Reason: " .. (metadata.ban_reason or ""))
|
||||
end
|
||||
end
|
||||
|
||||
return nk.json_encode({
|
||||
user_id = account.user.id,
|
||||
username = account.user.username,
|
||||
display_name = account.user.display_name,
|
||||
avatar_url = account.user.avatar_url,
|
||||
create_time = account.user.create_time,
|
||||
role = metadata.role or "player"
|
||||
})
|
||||
end
|
||||
|
||||
function user.rpc_update_user_profile(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
local request = nk.json_decode(payload)
|
||||
|
||||
local status, err = pcall(nk.account_update_id,
|
||||
context.user_id,
|
||||
nil,
|
||||
request.display_name or nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
request.avatar_url or nil,
|
||||
nil
|
||||
)
|
||||
|
||||
if not status then
|
||||
nk.logger_error("Failed to update profile: " .. tostring(err))
|
||||
error("Failed to update profile")
|
||||
end
|
||||
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function user.rpc_search_users(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
local request = nk.json_decode(payload or "{}")
|
||||
local query = request.query or ""
|
||||
|
||||
local users = {}
|
||||
local sql = ""
|
||||
local params = {}
|
||||
|
||||
if query == "" then
|
||||
sql = "SELECT id, username, display_name, metadata FROM users WHERE id != '00000000-0000-0000-0000-000000000000' ORDER BY create_time DESC LIMIT 100"
|
||||
else
|
||||
sql = "SELECT id, username, display_name, metadata FROM users WHERE (username ILIKE $1 OR display_name ILIKE $1) AND id != '00000000-0000-0000-0000-000000000000' ORDER BY create_time DESC LIMIT 100"
|
||||
params = {"%" .. query .. "%"}
|
||||
end
|
||||
|
||||
local status, rows = pcall(nk.sql_query, sql, params)
|
||||
if status and rows then
|
||||
for _, row in ipairs(rows) do
|
||||
local metadata = {}
|
||||
if row.metadata then
|
||||
local s, m = pcall(nk.json_decode, row.metadata)
|
||||
if s then metadata = m end
|
||||
end
|
||||
|
||||
table.insert(users, {
|
||||
user_id = row.id,
|
||||
username = row.username or "",
|
||||
display_name = row.display_name or row.username or "",
|
||||
avatar_url = metadata.avatar_url or ""
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return nk.json_encode({ users = users })
|
||||
end
|
||||
|
||||
function user.rpc_change_credentials(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
local req = nk.json_decode(payload or "{}")
|
||||
local account = nk.account_get_id(context.user_id)
|
||||
|
||||
if account.email then
|
||||
if not req.current_password then error("Current password required") end
|
||||
local status = pcall(nk.authenticate_email, account.email, req.current_password, false)
|
||||
if not status then error("Incorrect current password.") end
|
||||
nk.unlink_email(context.user_id, account.email, req.current_password)
|
||||
end
|
||||
|
||||
local status, err = pcall(nk.link_email, context.user_id, req.new_email, req.new_password)
|
||||
if not status then
|
||||
if account.email then pcall(nk.link_email, context.user_id, account.email, req.current_password) end
|
||||
error("Failed to set new credentials: " .. tostring(err))
|
||||
end
|
||||
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function user.rpc_send_lobby_invite(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
local req = nk.json_decode(payload or "{}")
|
||||
if not req.to_user_id or not req.match_id then error("Missing to_user_id or match_id") end
|
||||
|
||||
local sender = nk.account_get_id(context.user_id)
|
||||
local senderName = sender.user.display_name or sender.user.username or "Someone"
|
||||
|
||||
nk.notification_send(
|
||||
req.to_user_id,
|
||||
senderName .. " invited you to their lobby",
|
||||
nk.json_encode({ match_id = req.match_id, from_name = senderName }),
|
||||
1001,
|
||||
context.user_id,
|
||||
true
|
||||
)
|
||||
|
||||
nk.logger_info("Lobby invite sent from " .. context.user_id .. " to " .. req.to_user_id .. " for match " .. req.match_id)
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function user.rpc_send_friend_request(context, payload)
|
||||
if not context.user_id then error("Not authenticated") end
|
||||
|
||||
local request = nk.json_decode(payload or "{}")
|
||||
local targetUserId = request.user_id or ""
|
||||
|
||||
if targetUserId == "" then error("user_id is required") end
|
||||
if targetUserId == context.user_id then error("Cannot add yourself") end
|
||||
|
||||
local senderAccount = nk.account_get_id(context.user_id)
|
||||
local senderName = senderAccount.user.display_name or senderAccount.user.username or "Someone"
|
||||
|
||||
nk.notification_send(
|
||||
targetUserId,
|
||||
"Friend Request",
|
||||
nk.json_encode({ from_user_id = context.user_id, from_name = senderName }),
|
||||
1002,
|
||||
context.user_id,
|
||||
true
|
||||
)
|
||||
|
||||
nk.logger_info("Friend request notification sent from " .. context.user_id .. " to " .. targetUserId)
|
||||
return nk.json_encode({ success = true })
|
||||
end
|
||||
|
||||
function user.after_authenticate(context, out, payload)
|
||||
if not context.user_id then return end
|
||||
-- We store the last 10 logins in user metadata or a dedicated collection
|
||||
local login_entry = {
|
||||
time = os.time(),
|
||||
ip = context.client_ip or "unknown"
|
||||
}
|
||||
|
||||
local status, result = pcall(nk.storage_read, {{collection = "history", key = "logins", user_id = context.user_id}})
|
||||
local logins = {}
|
||||
if status and result and #result > 0 then
|
||||
logins = result[1].value.logins or {}
|
||||
end
|
||||
|
||||
table.insert(logins, 1, login_entry)
|
||||
-- Keep only last 20 logins to save space
|
||||
while #logins > 20 do table.remove(logins) end
|
||||
|
||||
pcall(nk.storage_write, {{
|
||||
collection = "history",
|
||||
key = "logins",
|
||||
user_id = context.user_id,
|
||||
value = { logins = logins },
|
||||
permission_read = 0,
|
||||
permission_write = 0
|
||||
}})
|
||||
end
|
||||
|
||||
function user.rpc_admin_get_user_history(context, payload)
|
||||
local utils = require("lua.utils")
|
||||
utils.require_admin(context)
|
||||
|
||||
local request = nk.json_decode(payload or "{}")
|
||||
local targetUserId = request.user_id
|
||||
|
||||
if not targetUserId then error("user_id is required") end
|
||||
|
||||
local history = {
|
||||
wallet_ledger = {},
|
||||
logins = {},
|
||||
matches = {}
|
||||
}
|
||||
|
||||
-- 1. Fetch Wallet Ledger (Economy History)
|
||||
local status_wallet, wallet_result = pcall(nk.wallet_ledger_list, targetUserId, 50)
|
||||
if status_wallet and wallet_result then
|
||||
history.wallet_ledger = wallet_result.items or {}
|
||||
end
|
||||
|
||||
-- 2. Fetch Login History
|
||||
local status_logins, login_result = pcall(nk.storage_read, {{collection = "history", key = "logins", user_id = targetUserId}})
|
||||
if status_logins and login_result and #login_result > 0 then
|
||||
history.logins = login_result[1].value.logins or {}
|
||||
end
|
||||
|
||||
-- 3. Fetch Match History (If stored in collection 'matches')
|
||||
local status_matches, match_result = pcall(nk.storage_list, targetUserId, "matches", 50, "")
|
||||
if status_matches and match_result then
|
||||
for _, obj in ipairs(match_result.objects or {}) do
|
||||
table.insert(history.matches, obj.value)
|
||||
end
|
||||
end
|
||||
|
||||
return nk.json_encode({ history = history })
|
||||
end
|
||||
|
||||
nk.register_rpc(user.rpc_get_user_profile, "get_user_profile")
|
||||
nk.register_rpc(user.rpc_update_user_profile, "update_user_profile")
|
||||
nk.register_rpc(user.rpc_search_users, "search_users")
|
||||
nk.register_rpc(user.rpc_change_credentials, "change_credentials")
|
||||
nk.register_rpc(user.rpc_send_lobby_invite, "send_lobby_invite")
|
||||
nk.register_rpc(user.rpc_send_friend_request, "send_friend_request")
|
||||
nk.register_rpc(user.rpc_admin_get_user_history, "admin_get_user_history")
|
||||
|
||||
nk.register_req_after(user.after_authenticate, "AuthenticateDevice")
|
||||
nk.register_req_after(user.after_authenticate, "AuthenticateEmail")
|
||||
nk.register_req_after(user.after_authenticate, "AuthenticateCustom")
|
||||
|
||||
nk.logger_info("LUA TEST: user module loaded")
|
||||
|
||||
return user
|
||||
@@ -0,0 +1,52 @@
|
||||
local nk = require("nakama")
|
||||
|
||||
local utils = {}
|
||||
|
||||
utils.ADMIN_ROLES = { ["admin"] = true, ["moderator"] = true, ["owner"] = true }
|
||||
|
||||
function utils.is_admin(context)
|
||||
if not context.user_id then return false end
|
||||
|
||||
local status, account = pcall(nk.account_get_id, context.user_id)
|
||||
if not status or not account then return false end
|
||||
|
||||
local metadata = {}
|
||||
if type(account.user.metadata) == "string" then
|
||||
status, metadata = pcall(nk.json_decode, account.user.metadata)
|
||||
if not status then metadata = {} end
|
||||
else
|
||||
metadata = account.user.metadata or {}
|
||||
end
|
||||
|
||||
local role = metadata.role or ""
|
||||
return utils.ADMIN_ROLES[role] == true
|
||||
end
|
||||
|
||||
function utils.is_match_host(context, match_id)
|
||||
if not context.user_id or not match_id then return false end
|
||||
local status, match = pcall(nk.match_get, match_id)
|
||||
if not status or not match then return false end
|
||||
|
||||
-- Needs to decode match.state if you're using authoritative matches
|
||||
-- Simplified for lua translation:
|
||||
local state = {}
|
||||
if match.state then
|
||||
status, state = pcall(nk.json_decode, match.state)
|
||||
if not status then state = {} end
|
||||
end
|
||||
return state.hostUserId == context.user_id
|
||||
end
|
||||
|
||||
function utils.require_admin(context)
|
||||
if not utils.is_admin(context) then
|
||||
error("Admin privileges required")
|
||||
end
|
||||
end
|
||||
|
||||
function utils.require_admin_or_host(context, match_id)
|
||||
if not utils.is_admin(context) and not utils.is_match_host(context, match_id) then
|
||||
error("Admin or host privileges required")
|
||||
end
|
||||
end
|
||||
|
||||
return utils
|
||||
Reference in New Issue
Block a user