feat(Core/Commands): add .account info (#27160)

This commit is contained in:
Andrew 2026-08-16 18:25:11 -03:00 committed by GitHub
parent 5fa7cb00fa
commit fe5b07e057
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 218 additions and 1 deletions

View file

@ -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);

View file

@ -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)', ' (не в сети)');

View file

@ -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);

View file

@ -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,

View file

@ -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);

View file

@ -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,

View file

@ -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
};

View file

@ -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

View file

@ -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<AccountIdentifier> 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<std::string>();
uint32 security = fields[1].Get<uint8>();
std::string eMail = fields[2].Get<std::string>();
std::string regMail = fields[3].Get<std::string>();
std::string lastIp = fields[4].Get<std::string>();
std::string lastLogin = fields[5].Get<std::string>();
int64 muteTime = fields[6].Get<uint64>();
std::string muteReason = fields[7].Get<std::string>();
std::string muteBy = fields[8].Get<std::string>();
uint32 failedLogins = fields[9].Get<uint32>();
uint8 locked = fields[10].Get<uint8>();
std::string OS = fields[11].Get<std::string>();
uint32 expansion = fields[12].Get<uint8>();
uint32 accountFlags = fields[13].Get<uint32>();
std::string joinDate = fields[14].Get<std::string>();
uint32 totalTime = fields[15].Get<uint32>();
std::string lockCountry = fields[16].Get<std::string>();
// 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<uint64>() ? 0 : banFields[0].Get<uint32>());
bannedBy = banFields[2].Get<std::string>();
banReason = banFields[3].Get<std::string>();
}
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<uint32>();
std::string name = charFields[1].Get<std::string>();
uint8 level = charFields[2].Get<uint8>();
uint8 raceId = charFields[3].Get<uint8>();
uint8 classId = charFields[4].Get<uint8>();
bool online = charFields[5].Get<bool>();
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)
{