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 channelId = utils.resolve_channel_id(channelId) 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_user_detail(context, payload) utils.require_admin(context) local request = nk.json_decode(payload or "{}") local userId = request.user_id or "" if userId == "" then error("user_id is required") end local account = nk.account_get_id(userId) local metadata = {} if account.user.metadata then local s, m = pcall(nk.json_decode, account.user.metadata) if s and m then metadata = m end end local friends = {} local okFriends, friendResult = pcall(nk.friends_list, userId, nil, 100, "") if okFriends and friendResult and friendResult.friends then for _, f in pairs(friendResult.friends) do table.insert(friends, { user_id = f.user.id, username = f.user.username, display_name = f.user.display_name, state = f.state }) end end local purchaseHistory = {} local okReceipts, receiptResult = pcall(nk.storage_list, userId, "receipts", 100, "") if okReceipts and receiptResult and receiptResult.objects then for _, obj in pairs(receiptResult.objects) do table.insert(purchaseHistory, { key = obj.key, value = obj.value, update_time = obj.update_time }) end end local collections = request.collections or {"profiles", "stats", "inventory", "receipts", "history", "matches", "inbox"} local storage = {} for _, collection in ipairs(collections) do local okStorage, storageResult = pcall(nk.storage_list, userId, collection, 100, "") storage[collection] = {} if okStorage and storageResult and storageResult.objects then for _, obj in pairs(storageResult.objects) do table.insert(storage[collection], { key = obj.key, value = obj.value, version = obj.version, update_time = obj.update_time }) end end end local walletLedger = {} local okLedger, ledgerResult = pcall(nk.wallet_ledger_list, userId, 50) if okLedger and ledgerResult then walletLedger = ledgerResult.items or {} end return nk.json_encode({ user = { user_id = account.user.id, username = account.user.username or "", display_name = account.user.display_name or "", avatar_url = account.user.avatar_url or "", lang_tag = account.user.lang_tag or "", location = account.user.location or "", timezone = account.user.timezone or "", create_time = account.user.create_time or "", update_time = account.user.update_time or "", metadata = metadata, wallet = account.wallet or {}, email = account.email or "", email_verified = account.email_verified or false }, friends = friends, purchases = purchaseHistory, wallet_ledger = walletLedger, storage = storage, subscription = metadata.subscription or metadata.subscriptions or {} }) end function admin.rpc_admin_update_user_identity(context, payload) utils.require_admin(context) local request = nk.json_decode(payload or "{}") local userId = request.user_id or "" if userId == "" then error("user_id is required") end local account = nk.account_get_id(userId) local metadata = {} if account.user.metadata then local s, m = pcall(nk.json_decode, account.user.metadata) if s and m then metadata = m end end if request.metadata and type(request.metadata) == "table" then for k, v in pairs(request.metadata) do metadata[k] = v end end nk.account_update_id( userId, request.username or account.user.username, request.display_name or account.user.display_name, request.timezone or account.user.timezone, request.location or account.user.location, request.lang_tag or account.user.lang_tag, request.avatar_url or account.user.avatar_url, nk.json_encode(metadata) ) return nk.json_encode({ success = true }) end function admin.rpc_admin_set_user_password(context, payload) utils.require_admin(context) local request = nk.json_decode(payload or "{}") local userId = request.user_id or "" local password = request.password or "" if userId == "" or password == "" then error("user_id and password are required") end local account = nk.account_get_id(userId) if not account.email or account.email == "" then error("User has no email credential") end nk.account_update_id(userId, nil, nil, nil, nil, nil, nil, nil, account.email, password) return nk.json_encode({ success = true }) 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 -- ============================================================================= -- Lobby Chat Management -- ============================================================================= function admin.rpc_admin_get_chat_config(context, payload) utils.require_admin(context) local configObjs = nk.storage_read({{ collection = "config", key = "lobby_chat", user_id = utils.SYSTEM_USER_ID }}) local config = { prefix = "", max_messages = 50, max_age_days = 0 } if configObjs and #configObjs > 0 and configObjs[1].value then local val = configObjs[1].value config.prefix = val.prefix or "" config.max_messages = val.max_messages or 50 config.max_age_days = val.max_age_days or 0 end return nk.json_encode({ config = config }) end function admin.rpc_admin_set_chat_config(context, payload) utils.require_admin(context) local request = nk.json_decode(payload or "{}") local config = { prefix = request.prefix or "", max_messages = request.max_messages or 50, max_age_days = request.max_age_days or 0 } nk.storage_write({{ collection = "config", key = "lobby_chat", user_id = utils.SYSTEM_USER_ID, value = config, permission_read = 2, permission_write = 0 }}) nk.logger_info("[AdminChat] Chat config updated by " .. context.user_id) return nk.json_encode({ success = true }) end function admin.rpc_admin_purge_old_messages(context, payload) utils.require_admin(context) local request = nk.json_decode(payload or "{}") local channelId = request.channel_id or "" local maxAgeDays = request.max_age_days or 0 if channelId == "" then error("channel_id is required") end channelId = utils.resolve_channel_id(channelId) if maxAgeDays <= 0 then error("max_age_days must be > 0") end local cutoff = os.time() - (maxAgeDays * 86400) 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 -- Parse create_time to compare against cutoff local msgTime = 0 if msg.create_time then -- Try ISO format: "2024-01-15T10:30:00Z" local y, m, d, hh, mm, ss = msg.create_time:match("(%d+)%-(%d+)%-(%d+)T(%d+):(%d+):(%d+)") if y then msgTime = os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d), hour = tonumber(hh), min = tonumber(mm), sec = tonumber(ss) }) else -- Try unix timestamp string msgTime = tonumber(msg.create_time) or 0 end end if msgTime > 0 and msgTime < cutoff then pcall(nk.channel_message_remove, channelId, msg.message_id) deleted = deleted + 1 end end cursor = result.next_cursor or "" until cursor == "" nk.logger_info("[AdminChat] Purged " .. deleted .. " messages older than " .. maxAgeDays .. " days by " .. context.user_id) return nk.json_encode({ success = true, deleted = deleted }) end function admin.rpc_admin_list_channel_messages(context, payload) utils.require_admin(context) local request = nk.json_decode(payload or "{}") local channelId = request.channel_id or "" local limit = request.limit or 50 local cursor = request.cursor or "" local forward = request.forward == nil and true or request.forward if channelId == "" then error("channel_id is required") end channelId = utils.resolve_channel_id(channelId) local status, result = pcall(nk.channel_messages_list, channelId, limit, forward, cursor) if not status then error("Failed to list messages: " .. tostring(result)) end local msgs = {} if result and result.messages then for _, msg in pairs(result.messages) do table.insert(msgs, { message_id = msg.message_id, sender_id = msg.sender_id, username = msg.username, content = msg.content, create_time = msg.create_time, update_time = msg.update_time, channel_id = msg.channel_id }) end end return nk.json_encode({ messages = msgs, channel_id = channelId, next_cursor = result.next_cursor or "", cache_cursor = result.cache_cursor or "" }) end function admin.rpc_admin_delete_channel_message(context, payload) utils.require_admin(context) local request = nk.json_decode(payload or "{}") local channelId = request.channel_id or "" local messageId = request.message_id or "" if channelId == "" or messageId == "" then error("channel_id and message_id are required") end channelId = utils.resolve_channel_id(channelId) local status, err = pcall(nk.channel_message_remove, channelId, messageId) if not status then error("Failed to delete message: " .. tostring(err)) end nk.logger_info("[AdminChat] Deleted message " .. messageId .. " from channel " .. channelId .. " by " .. context.user_id) return nk.json_encode({ success = true }) 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.register_rpc(admin.rpc_admin_get_user_detail, "admin_get_user_detail") nk.register_rpc(admin.rpc_admin_update_user_identity, "admin_update_user_identity") nk.register_rpc(admin.rpc_admin_set_user_password, "admin_set_user_password") nk.register_rpc(admin.rpc_admin_get_chat_config, "admin_get_chat_config") nk.register_rpc(admin.rpc_admin_set_chat_config, "admin_set_chat_config") nk.register_rpc(admin.rpc_admin_purge_old_messages, "admin_purge_old_messages") nk.register_rpc(admin.rpc_admin_list_channel_messages, "admin_list_channel_messages") nk.register_rpc(admin.rpc_admin_delete_channel_message, "admin_delete_channel_message") nk.logger_info("LUA TEST: admin module loaded successfully") return admin