diff --git a/data/sql/updates/pending_db_auth/rev_1786700270269177900.sql b/data/sql/updates/pending_db_auth/rev_1786700270269177900.sql new file mode 100644 index 000000000..a9c19237c --- /dev/null +++ b/data/sql/updates/pending_db_auth/rev_1786700270269177900.sql @@ -0,0 +1,7 @@ +DELETE FROM `rbac_permissions` WHERE `id` = 945; +INSERT INTO `rbac_permissions` (`id`, `name`) VALUES +(945, 'Command: account info'); + +DELETE FROM `rbac_linked_permissions` WHERE `linkedId` = 945; +INSERT INTO `rbac_linked_permissions` (`id`, `linkedId`) VALUES +(197, 945); diff --git a/data/sql/updates/pending_db_world/rev_1786700270405808000.sql b/data/sql/updates/pending_db_world/rev_1786700270405808000.sql new file mode 100644 index 000000000..f9cb7b7be --- /dev/null +++ b/data/sql/updates/pending_db_world/rev_1786700270405808000.sql @@ -0,0 +1,15 @@ +DELETE FROM `command` WHERE `name` = 'account info'; +INSERT INTO `command` (`name`, `security`, `help`) VALUES +('account info', 2, 'Syntax: .account info [$account]\nDisplays account level information for $account, given either as an account name or an account id. Defaults to the account of the selected player, or your own account when nothing is selected.'); + +DELETE FROM `acore_string` WHERE `entry` IN (35471, 35472, 35473, 35474, 35475, 35476, 35477, 35478, 35479); +INSERT INTO `acore_string` (`entry`, `content_default`, `locale_koKR`, `locale_frFR`, `locale_deDE`, `locale_zhCN`, `locale_zhTW`, `locale_esES`, `locale_esMX`, `locale_ruRU`) VALUES +(35471, '| Created: {}', '| 생성일: {}', '| Créé le: {}', '| Erstellt: {}', '| 创建时间: {}', '| 建立時間: {}', '| Creada: {}', '| Creada: {}', '| Создан: {}'), +(35472, '| Expansion: {}', '| 확장팩: {}', '| Extension: {}', '| Erweiterung: {}', '| 资料片: {}', '| 資料片: {}', '| Expansión: {}', '| Expansión: {}', '| Дополнение: {}'), +(35473, '| Locked to country: {}', '| 국가 잠금: {}', '| Verrouillé au pays: {}', '| Gesperrt auf Land: {}', '| 锁定国家: {}', '| 鎖定國家: {}', '| Bloqueada al país: {}', '| Bloqueada al país: {}', '| Привязан к стране: {}'), +(35474, '|-- Banned: (Reason: {}, Time: {}, By: {})', '|-- 차단됨: (사유: {}, 기간: {}, 처리자: {})', '|-- Banni: (Raison: {}, Temps: {}, Par: {})', '|-- Gesperrt: (Grund: {}, Zeit: {}, Von: {})', '|-- 封禁: (原因: {}, 时间: {}, 操作者: {})', '|-- 封禁: (原因: {}, 時間: {}, 操作者: {})', '|-- Baneado: (Razón: {}, Tiempo: {}, Por: {})', '|-- Baneado: (Razón: {}, Tiempo: {}, Por: {})', '|-- Заблокирован: (Причина: {}, Время: {}, Кем: {})'), +(35475, '| Characters on this realm: {}', '| 이 서버의 캐릭터: {}', '| Personnages sur ce royaume: {}', '| Charaktere auf diesem Realm: {}', '| 该服务器上的角色: {}', '| 該伺服器上的角色: {}', '| Personajes en este reino: {}', '| Personajes en este reino: {}', '| Персонажи в этом мире: {}'), +(35476, '|-- {}{} (GUID: {}) - Level {} {} {}', '|-- {}{} (GUID: {}) - 레벨 {} {} {}', '|-- {}{} (GUID: {}) - Niveau {} {} {}', '|-- {}{} (GUID: {}) - Level {} {} {}', '|-- {}{} (GUID: {}) - 等级 {} {} {}', '|-- {}{} (GUID: {}) - 等級 {} {} {}', '|-- {}{} (GUID: {}) - Nivel {} {} {}', '|-- {}{} (GUID: {}) - Nivel {} {} {}', '|-- {}{} (GUID: {}) - Уровень {} {} {}'), +(35477, '| No characters on this realm.', '| 이 서버에 캐릭터가 없습니다.', '| Aucun personnage sur ce royaume.', '| Keine Charaktere auf diesem Realm.', '| 该服务器上没有角色。', '| 該伺服器上沒有角色。', '| Sin personajes en este reino.', '| Sin personajes en este reino.', '| Нет персонажей в этом мире.'), +(35478, '| Current IP: {}', '| 현재 IP: {}', '| IP actuelle: {}', '| Aktuelle IP: {}', '| 当前 IP: {}', '| 目前 IP: {}', '| IP actual: {}', '| IP actual: {}', '| Текущий IP: {}'), +(35479, ' (offline)', ' (오프라인)', ' (hors ligne)', ' (offline)', ' (离线)', ' (離線)', ' (sin conexión)', ' (sin conexión)', ' (не в сети)'); diff --git a/src/server/database/Database/Implementation/CharacterDatabase.cpp b/src/server/database/Database/Implementation/CharacterDatabase.cpp index 80b2b6679..bef2555d0 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.cpp +++ b/src/server/database/Database/Implementation/CharacterDatabase.cpp @@ -409,6 +409,7 @@ void CharacterDatabaseConnection::DoPrepareStatements() PrepareStatement(CHAR_SEL_CHAR_DEL_INFO_BY_NAME_LIMIT, "SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate, level FROM characters WHERE deleteDate IS NOT NULL AND deleteInfos_Name LIKE CONCAT('%%', ?, '%%') ORDER BY deleteDate DESC LIMIT 51", CONNECTION_SYNCH); PrepareStatement(CHAR_SEL_CHAR_DEL_INFO, "SELECT guid, deleteInfos_Name, deleteInfos_Account, deleteDate, level FROM characters WHERE deleteDate IS NOT NULL ORDER BY deleteDate DESC LIMIT 51", CONNECTION_SYNCH); PrepareStatement(CHAR_SEL_CHARS_BY_ACCOUNT_ID, "SELECT guid FROM characters WHERE account = ?", CONNECTION_SYNCH); + PrepareStatement(CHAR_SEL_ACCOUNT_INFO_CHARS, "SELECT guid, name, level, race, class, online FROM characters WHERE account = ? AND deleteDate IS NULL ORDER BY level DESC, name ASC", CONNECTION_SYNCH); PrepareStatement(CHAR_SEL_CHAR_PINFO, "SELECT totaltime, level, money, account, race, class, map, zone, gender, health, playerFlags FROM characters WHERE guid = ?", CONNECTION_SYNCH); PrepareStatement(CHAR_SEL_PINFO_BANS, "SELECT unbandate, bandate = unbandate, bannedby, banreason FROM character_banned WHERE guid = ? AND active ORDER BY bandate ASC LIMIT 1", CONNECTION_SYNCH); PrepareStatement(CHAR_SEL_PINFO_MAILS, "SELECT SUM(CASE WHEN (checked & 1) THEN 1 ELSE 0 END) AS 'readmail', COUNT(*) AS 'totalmail' FROM mail WHERE `receiver` = ?", CONNECTION_SYNCH); diff --git a/src/server/database/Database/Implementation/CharacterDatabase.h b/src/server/database/Database/Implementation/CharacterDatabase.h index f9ba5dc6c..9367084e4 100644 --- a/src/server/database/Database/Implementation/CharacterDatabase.h +++ b/src/server/database/Database/Implementation/CharacterDatabase.h @@ -337,6 +337,7 @@ enum CharacterDatabaseStatements : uint32 CHAR_SEL_CHAR_DEL_INFO, CHAR_SEL_CHARS_BY_ACCOUNT_ID, + CHAR_SEL_ACCOUNT_INFO_CHARS, CHAR_SEL_CHAR_PINFO, CHAR_SEL_PINFO_XP, CHAR_SEL_PINFO_MAILS, diff --git a/src/server/database/Database/Implementation/LoginDatabase.cpp b/src/server/database/Database/Implementation/LoginDatabase.cpp index fa589fa23..e2e7d25bf 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.cpp +++ b/src/server/database/Database/Implementation/LoginDatabase.cpp @@ -106,6 +106,7 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_UPD_SET_ACCOUNT_FLAG, "UPDATE account SET Flags = ? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_SEL_PINFO, "SELECT a.username, aa.gmlevel, a.email, a.reg_mail, a.last_ip, DATE_FORMAT(a.last_login, '%Y-%m-%d %T'), a.mutetime, a.mutereason, a.muteby, a.failed_logins, a.locked, a.OS FROM account a LEFT JOIN account_access aa ON (a.id = aa.id AND (aa.RealmID = ? OR aa.RealmID = -1)) WHERE a.id = ?", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_PINFO_BANS, "SELECT unbandate, bandate = unbandate, bannedby, banreason FROM account_banned WHERE id = ? AND active ORDER BY bandate ASC LIMIT 1", CONNECTION_SYNCH); + PrepareStatement(LOGIN_SEL_ACCOUNT_INFO_DETAILED, "SELECT a.username, aa.gmlevel, a.email, a.reg_mail, a.last_ip, DATE_FORMAT(a.last_login, '%Y-%m-%d %T'), a.mutetime, a.mutereason, a.muteby, a.failed_logins, a.locked, a.OS, a.expansion, a.Flags, DATE_FORMAT(a.joindate, '%Y-%m-%d %T'), a.totaltime, a.lock_country FROM account a LEFT JOIN account_access aa ON (a.id = aa.id AND (aa.RealmID = ? OR aa.RealmID = -1)) WHERE a.id = ? ORDER BY aa.RealmID DESC LIMIT 1", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_GM_ACCOUNTS, "SELECT a.username, aa.gmlevel FROM account a, account_access aa WHERE a.id=aa.id AND aa.gmlevel >= ? AND (aa.realmid = -1 OR aa.realmid = ?)", CONNECTION_SYNCH); PrepareStatement(LOGIN_SEL_ACCOUNT_INFO, "SELECT a.username, a.last_ip, aa.gmlevel, a.expansion FROM account a LEFT JOIN account_access aa ON (a.id = aa.id) WHERE a.id = ? ORDER BY a.last_ip", CONNECTION_SYNCH); // Only used in ".account onlinelist" command PrepareStatement(LOGIN_SEL_ACCOUNT_ACCESS_GMLEVEL_TEST, "SELECT 1 FROM account_access WHERE id = ? AND gmlevel > ?", CONNECTION_SYNCH); diff --git a/src/server/database/Database/Implementation/LoginDatabase.h b/src/server/database/Database/Implementation/LoginDatabase.h index 99e057b0c..65c10a8bb 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.h +++ b/src/server/database/Database/Implementation/LoginDatabase.h @@ -90,6 +90,7 @@ enum LoginDatabaseStatements : uint32 LOGIN_UPD_SET_ACCOUNT_FLAG, LOGIN_SEL_PINFO, LOGIN_SEL_PINFO_BANS, + LOGIN_SEL_ACCOUNT_INFO_DETAILED, LOGIN_SEL_GM_ACCOUNTS, LOGIN_SEL_ACCOUNT_INFO, LOGIN_SEL_ACCOUNT_ACCESS_GMLEVEL_TEST, diff --git a/src/server/game/Accounts/RBAC.h b/src/server/game/Accounts/RBAC.h index 58481ef8d..73252653c 100644 --- a/src/server/game/Accounts/RBAC.h +++ b/src/server/game/Accounts/RBAC.h @@ -706,6 +706,7 @@ enum RBACPermissions RBAC_PERM_COMMAND_ACCOUNT_FLAG_LIST = 942, RBAC_PERM_COMMAND_ACCOUNT_FLAG_ADD = 943, RBAC_PERM_COMMAND_ACCOUNT_FLAG_REMOVE = 944, + RBAC_PERM_COMMAND_ACCOUNT_INFO = 945, // custom permissions 1000+ RBAC_PERM_MAX }; diff --git a/src/server/game/Miscellaneous/Language.h b/src/server/game/Miscellaneous/Language.h index b405d0e24..6c44df656 100644 --- a/src/server/game/Miscellaneous/Language.h +++ b/src/server/game/Miscellaneous/Language.h @@ -1545,6 +1545,17 @@ enum AcoreStrings LANG_ACCOUNT_FLAG_INVALID = 35467, LANG_ACCOUNT_FLAG_RESERVED = 35468, LANG_ACCOUNT_FLAG_ADDED = 35469, - LANG_ACCOUNT_FLAG_REMOVED = 35470 + LANG_ACCOUNT_FLAG_REMOVED = 35470, + + // Account info command + LANG_ACCOUNT_INFO_JOINDATE = 35471, + LANG_ACCOUNT_INFO_EXPANSION = 35472, + LANG_ACCOUNT_INFO_LOCK_COUNTRY = 35473, + LANG_ACCOUNT_INFO_BANNED = 35474, + LANG_ACCOUNT_INFO_CHARS = 35475, + LANG_ACCOUNT_INFO_CHAR_ENTRY = 35476, + LANG_ACCOUNT_INFO_NO_CHARS = 35477, + LANG_ACCOUNT_INFO_CURRENT_IP = 35478, + LANG_ACCOUNT_INFO_CHAR_OFFLINE = 35479 }; #endif diff --git a/src/server/scripts/Commands/cs_account.cpp b/src/server/scripts/Commands/cs_account.cpp index 45a20d078..294608c89 100644 --- a/src/server/scripts/Commands/cs_account.cpp +++ b/src/server/scripts/Commands/cs_account.cpp @@ -22,6 +22,9 @@ #include "CommandScript.h" #include "Common.h" #include "CryptoGenerics.h" +#include "DBCStores.h" +#include "DatabaseEnv.h" +#include "GameTime.h" #include "IPLocation.h" #include "Language.h" #include "Player.h" @@ -89,6 +92,7 @@ public: { "create", HandleAccountCreateCommand, rbac::RBAC_PERM_COMMAND_ACCOUNT_CREATE, Console::Yes }, { "delete", HandleAccountDeleteCommand, rbac::RBAC_PERM_COMMAND_ACCOUNT_DELETE, Console::Yes }, { "flag", accountFlagCommandTable }, + { "info", HandleAccountInfoCommand, rbac::RBAC_PERM_COMMAND_ACCOUNT_INFO, Console::Yes }, { "onlinelist", HandleAccountOnlineListCommand, rbac::RBAC_PERM_COMMAND_ACCOUNT_ONLINE_LIST, Console::Yes }, { "lock", accountLockCommandTable }, { "set", accountSetCommandTable }, @@ -681,6 +685,181 @@ public: return true; } + // Show account-wide info, optionally for another account (by name or id) + static bool HandleAccountInfoCommand(ChatHandler* handler, Optional account) + { + uint32 accountId; + std::string accountName; + + if (account) + { + accountId = account->GetID(); + accountName = account->GetName(); + } + else if (Player* target = handler->getSelectedPlayerOrSelf()) + { + accountId = target->GetSession()->GetAccountId(); + AccountMgr::GetName(accountId, accountName); + } + else + { + handler->SendErrorMessage(LANG_CMD_SYNTAX); + return false; + } + + // The strong check rejects equal security, so inspecting your own account has to be exempted + if (handler->GetSession() && handler->GetSession()->GetAccountId() != accountId && + handler->HasLowerSecurityAccount(nullptr, accountId, true)) + return false; + + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_ACCOUNT_INFO_DETAILED); + stmt->SetData(0, int32(realm.Id.Realm)); + stmt->SetData(1, accountId); + + PreparedQueryResult result = LoginDatabase.Query(stmt); + if (!result) + { + handler->SendErrorMessage(LANG_ACCOUNT_NOT_EXIST, accountName); + return false; + } + + Field* fields = result->Fetch(); + accountName = fields[0].Get(); + uint32 security = fields[1].Get(); + std::string eMail = fields[2].Get(); + std::string regMail = fields[3].Get(); + std::string lastIp = fields[4].Get(); + std::string lastLogin = fields[5].Get(); + int64 muteTime = fields[6].Get(); + std::string muteReason = fields[7].Get(); + std::string muteBy = fields[8].Get(); + uint32 failedLogins = fields[9].Get(); + uint8 locked = fields[10].Get(); + std::string OS = fields[11].Get(); + uint32 expansion = fields[12].Get(); + uint32 accountFlags = fields[13].Get(); + std::string joinDate = fields[14].Get(); + uint32 totalTime = fields[15].Get(); + std::string lockCountry = fields[16].Get(); + + // Empty unless the account is online, in which case it holds the address of the live connection + std::string currentIp; + uint32 latency = 0; + + if (WorldSession* targetSession = sWorldSessionMgr->FindSession(accountId)) + { + latency = targetSession->GetLatency(); + muteTime = targetSession->m_muteTime; + accountFlags = targetSession->GetAccountFlags(); + currentIp = targetSession->GetRemoteAddress(); + } + + if (!handler->GetSession() || handler->GetSession()->GetSecurity() >= AccountTypes(security)) + { + if (IpLocationRecord const* location = sIPLocation->GetLocationRecord(lastIp)) + lastIp.append(" (").append(location->CountryName).append(")"); + + if (!currentIp.empty()) + if (IpLocationRecord const* location = sIPLocation->GetLocationRecord(currentIp)) + currentIp.append(" (").append(location->CountryName).append(")"); + } + else + { + eMail = handler->GetAcoreString(LANG_UNAUTHORIZED); + regMail = handler->GetAcoreString(LANG_UNAUTHORIZED); + lastIp = handler->GetAcoreString(LANG_UNAUTHORIZED); + lastLogin = handler->GetAcoreString(LANG_UNAUTHORIZED); + + if (!currentIp.empty()) + currentIp = handler->GetAcoreString(LANG_UNAUTHORIZED); + } + + int64 banTime = -1; + std::string banReason = handler->GetAcoreString(LANG_NO_REASON); + std::string bannedBy = handler->GetAcoreString(LANG_UNKNOWN); + + LoginDatabasePreparedStatement* banStmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_PINFO_BANS); + banStmt->SetData(0, accountId); + + if (PreparedQueryResult banResult = LoginDatabase.Query(banStmt)) + { + Field* banFields = banResult->Fetch(); + banTime = int64(banFields[1].Get() ? 0 : banFields[0].Get()); + bannedBy = banFields[2].Get(); + banReason = banFields[3].Get(); + } + + handler->PSendSysMessage(LANG_PINFO_ACC_ACCOUNT, accountName, accountId, security); + + if (accountFlags) + { + handler->PSendSysMessage(LANG_ACCOUNT_FLAGS_PINFO); + for (uint8 i = 0; i < MAX_ACCOUNT_FLAG; ++i) + if (accountFlags & (uint32(1) << i)) + handler->PSendSysMessage(LANG_SUBCMDS_LIST_ENTRY, accountFlagNames[i].full); + } + + if (banTime >= 0) + handler->PSendSysMessage(LANG_ACCOUNT_INFO_BANNED, banReason, + banTime > 0 ? secsToTimeString(banTime - GameTime::GetGameTime().count(), true) : handler->GetAcoreString(LANG_PERMANENTLY), bannedBy); + + // mutetime is only cleared once the muted player logs back in, so an expired mute can still be stored + if (muteTime > GameTime::GetGameTime().count()) + handler->PSendSysMessage(LANG_PINFO_MUTED, muteReason, secsToTimeString(muteTime - GameTime::GetGameTime().count(), true), muteBy); + + handler->PSendSysMessage(LANG_ACCOUNT_INFO_JOINDATE, joinDate); + handler->PSendSysMessage(LANG_PINFO_ACC_LASTLOGIN, lastLogin, failedLogins); + handler->PSendSysMessage(LANG_PINFO_ACC_OS, OS, latency); + handler->PSendSysMessage(LANG_PINFO_ACC_REGMAILS, regMail, eMail); + handler->PSendSysMessage(LANG_PINFO_ACC_IP, lastIp, locked ? handler->GetAcoreString(LANG_YES) : handler->GetAcoreString(LANG_NO)); + + if (!currentIp.empty()) + handler->PSendSysMessage(LANG_ACCOUNT_INFO_CURRENT_IP, currentIp); + + // "00" is the default value meaning no country lock is set + if (lockCountry != "00") + handler->PSendSysMessage(LANG_ACCOUNT_INFO_LOCK_COUNTRY, lockCountry); + + handler->PSendSysMessage(LANG_ACCOUNT_INFO_EXPANSION, expansion); + handler->PSendSysMessage(LANG_PINFO_CHR_PLAYEDTIME, secsToTimeString(totalTime, true)); + + CharacterDatabasePreparedStatement* charStmt = CharacterDatabase.GetPreparedStatement(CHAR_SEL_ACCOUNT_INFO_CHARS); + charStmt->SetData(0, accountId); + + PreparedQueryResult charResult = CharacterDatabase.Query(charStmt); + if (!charResult) + { + handler->PSendSysMessage(LANG_ACCOUNT_INFO_NO_CHARS); + return true; + } + + handler->PSendSysMessage(LANG_ACCOUNT_INFO_CHARS, uint32(charResult->GetRowCount())); + + LocaleConstant locale = handler->GetSessionDbcLocale(); + + do + { + Field* charFields = charResult->Fetch(); + ObjectGuid::LowType guid = charFields[0].Get(); + std::string name = charFields[1].Get(); + uint8 level = charFields[2].Get(); + uint8 raceId = charFields[3].Get(); + uint8 classId = charFields[4].Get(); + bool online = charFields[5].Get(); + + ChrRacesEntry const* race = sChrRacesStore.LookupEntry(raceId); + ChrClassesEntry const* cls = sChrClassesStore.LookupEntry(classId); + + // Own marker rather than LANG_OFFLINE, whose leading space is missing in some locales + handler->PSendSysMessage(LANG_ACCOUNT_INFO_CHAR_ENTRY, handler->playerLink(name), + online ? "" : handler->GetAcoreString(LANG_ACCOUNT_INFO_CHAR_OFFLINE), guid, uint32(level), + race ? race->name[locale] : handler->GetAcoreString(LANG_UNKNOWN), + cls ? cls->name[locale] : handler->GetAcoreString(LANG_UNKNOWN)); + } while (charResult->NextRow()); + + return true; + } + /// Set/Unset the expansion level for an account static bool HandleAccountSetAddonCommand(ChatHandler* handler, char const* args) {