mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 19:03:43 +00:00
feat: web dashboard and playground work
The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch: accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes they need. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,16 +6,11 @@
|
||||
#include "Logger.h"
|
||||
#include "dPlatforms.h"
|
||||
|
||||
namespace {
|
||||
std::string databaseName;
|
||||
sql::Properties properties;
|
||||
sql::Driver* driver = nullptr;
|
||||
sql::Connection* con = nullptr;
|
||||
};
|
||||
|
||||
void MySQLDatabase::Connect() {
|
||||
LOG("Using MySQL database");
|
||||
driver = sql::mariadb::get_driver_instance();
|
||||
auto* driver = sql::mariadb::get_driver_instance();
|
||||
sql::Properties properties;
|
||||
|
||||
// The mariadb connector is *supposed* to handle unix:// and pipe:// prefixes to hostName, but there are bugs where
|
||||
// 1) it tries to parse a database from the connection string (like in tcp://localhost:3001/darkflame) based on the
|
||||
@@ -40,64 +35,68 @@ void MySQLDatabase::Connect() {
|
||||
properties["password"] = Game::config->GetValue("mysql_password").c_str();
|
||||
properties["autoReconnect"] = "true";
|
||||
|
||||
databaseName = Game::config->GetValue("mysql_database").c_str();
|
||||
const std::string databaseName = Game::config->GetValue("mysql_database");
|
||||
// `connect(const Properties& props)` segfaults in windows debug, but
|
||||
// `connect(const SQLString& host, const SQLString& user, const SQLString& pwd)` doesn't handle pipes/unix sockets correctly
|
||||
#if defined(DARKFLAME_PLATFORM_WIN32) && defined(_DEBUG)
|
||||
con = driver->connect(properties["hostName"].c_str(), properties["user"].c_str(), properties["password"].c_str());
|
||||
m_Con = driver->connect(properties["hostName"].c_str(), properties["user"].c_str(), properties["password"].c_str());
|
||||
#else
|
||||
con = driver->connect(properties);
|
||||
m_Con = driver->connect(properties);
|
||||
#endif
|
||||
con->setSchema(databaseName.c_str());
|
||||
m_Con->setSchema(databaseName.c_str());
|
||||
}
|
||||
|
||||
void MySQLDatabase::Destroy(std::string source) {
|
||||
if (!con) return;
|
||||
if (!m_Con) return;
|
||||
|
||||
if (source.empty()) LOG("Destroying MySQL connection!");
|
||||
else LOG("Destroying MySQL connection from %s!", source.c_str());
|
||||
|
||||
con->close();
|
||||
delete con;
|
||||
con = nullptr;
|
||||
m_Con->close();
|
||||
delete m_Con;
|
||||
m_Con = nullptr;
|
||||
}
|
||||
|
||||
void MySQLDatabase::ExecuteCustomQuery(const std::string_view query) {
|
||||
std::unique_ptr<sql::Statement>(con->createStatement())->execute(query.data());
|
||||
std::unique_ptr<sql::Statement>(m_Con->createStatement())->execute(query.data());
|
||||
}
|
||||
|
||||
sql::PreparedStatement* MySQLDatabase::CreatePreppedStmt(const std::string& query) {
|
||||
if (!con) {
|
||||
Database::Get()->Connect();
|
||||
if (!m_Con) {
|
||||
Connect();
|
||||
LOG("Trying to reconnect to MySQL");
|
||||
}
|
||||
|
||||
if (!con->isValid() || con->isClosed()) {
|
||||
delete con;
|
||||
if (!m_Con->isValid() || m_Con->isClosed()) {
|
||||
delete m_Con;
|
||||
|
||||
con = nullptr;
|
||||
m_Con = nullptr;
|
||||
|
||||
Database::Get()->Connect();
|
||||
Connect();
|
||||
LOG("Trying to reconnect to MySQL from invalid or closed connection");
|
||||
}
|
||||
|
||||
return con->prepareStatement(sql::SQLString(query.c_str(), query.length()));
|
||||
return m_Con->prepareStatement(sql::SQLString(query.c_str(), query.length()));
|
||||
}
|
||||
|
||||
void MySQLDatabase::Commit() {
|
||||
con->commit();
|
||||
m_Con->commit();
|
||||
}
|
||||
|
||||
bool MySQLDatabase::GetAutoCommit() {
|
||||
// TODO This should not just access a pointer. A future PR should update this
|
||||
// to check for null and throw an error if the connection is not valid.
|
||||
return con->getAutoCommit();
|
||||
return m_Con->getAutoCommit();
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetAutoCommit(bool value) {
|
||||
// TODO This should not just access a pointer. A future PR should update this
|
||||
// to check for null and throw an error if the connection is not valid.
|
||||
con->setAutoCommit(value);
|
||||
m_Con->setAutoCommit(value);
|
||||
}
|
||||
|
||||
void MySQLDatabase::Rollback() {
|
||||
m_Con->rollback();
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteCharacter(const LWOOBJID characterId) {
|
||||
@@ -110,5 +109,9 @@ void MySQLDatabase::DeleteCharacter(const LWOOBJID characterId) {
|
||||
ExecuteDelete("DELETE FROM ugc WHERE character_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM activity_log WHERE character_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM mail WHERE receiver_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM ignore_list WHERE player_id=? OR ignored_player_id=?;", characterId, characterId);
|
||||
ExecuteDelete("DELETE FROM ugc_modular_build WHERE character_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM pet_names WHERE owner_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM player_positions WHERE character_id=?;", characterId);
|
||||
ExecuteDelete("DELETE FROM charinfo WHERE id=? LIMIT 1;", characterId);
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ public:
|
||||
void Commit() override;
|
||||
bool GetAutoCommit() override;
|
||||
void SetAutoCommit(bool value) override;
|
||||
void Rollback() override;
|
||||
void ExecuteCustomQuery(const std::string_view query) override;
|
||||
|
||||
// Overloaded queries
|
||||
@@ -74,6 +75,7 @@ public:
|
||||
std::optional<IAccounts::Info> GetAccountInfo(const std::string_view username) override;
|
||||
void InsertNewCharacter(const ICharInfo::Info info) override;
|
||||
void InsertCharacterXml(const LWOOBJID accountId, const std::string_view lxfml) override;
|
||||
std::string GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
std::vector<LWOOBJID> GetAccountCharacterIds(LWOOBJID accountId) override;
|
||||
void DeleteCharacter(const LWOOBJID characterId) override;
|
||||
void SetCharacterName(const LWOOBJID characterId, const std::string_view name) override;
|
||||
@@ -93,6 +95,212 @@ public:
|
||||
void RemoveModel(const LWOOBJID& modelId) override;
|
||||
void UpdatePerformanceCost(const LWOZONEID& zoneId, const float performanceCost) override;
|
||||
void InsertNewBugReport(const IBugReports::Info& info) override;
|
||||
std::string GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true, int8_t resolvedFilter = -1) override;
|
||||
nlohmann::json GetBugReport(const uint32_t id) override;
|
||||
void ResolveBugReport(const uint32_t id, const uint32_t resolverAccountId, const std::string_view resolution) override;
|
||||
nlohmann::json GetPlayKey(const int32_t playkeyId) override;
|
||||
void UpdatePlayKey(const int32_t playkeyId, const uint32_t uses, const std::string_view notes, const bool active) override;
|
||||
void DeletePlayKey(const int32_t playkeyId) override;
|
||||
std::vector<std::pair<LWOOBJID, std::string>> GetCharacterIdsAndNames() override;
|
||||
nlohmann::json GetPendingNamesTable(const uint32_t start, const uint32_t length) override;
|
||||
void SetCharacterPermissionMap(const LWOOBJID characterId, const uint64_t permissionMap) override;
|
||||
IDashboardStats::Snapshot GetDashboardSnapshot() override;
|
||||
void RecordEconomy(const std::vector<CurrencyFlow>& currency, const std::vector<UScoreFlow>& uscore,
|
||||
const std::vector<ItemFlow>& items, const std::vector<ItemTransfer>& transfers, const std::vector<MapEvent>& mapEvents,
|
||||
const std::vector<PlayerStat>& stats) override;
|
||||
nlohmann::json GetCurrencyFlows(uint32_t fromDay, uint32_t toDay, bool excludeStaff) override;
|
||||
nlohmann::json GetUScoreFlows(uint32_t fromDay, uint32_t toDay, bool excludeStaff) override;
|
||||
nlohmann::json GetItemFlows(uint32_t fromDay, uint32_t toDay, LOT lot, bool excludeStaff) override;
|
||||
nlohmann::json GetTopEarners(uint32_t fromDay, uint32_t toDay, uint32_t limit, bool excludeStaff) override;
|
||||
nlohmann::json GetTopItems(uint32_t fromDay, uint32_t toDay, uint32_t limit, bool excludeStaff) override;
|
||||
nlohmann::json GetTransfers(uint32_t start, uint32_t length, LWOOBJID characterId, LOT lot) override;
|
||||
nlohmann::json GetTransfersForItems(const std::vector<LWOOBJID>& itemIds) override;
|
||||
void ForEachMailAttachment(const std::function<void(const MailAttachment&)>& visit) override;
|
||||
std::vector<Webhook> GetWebhooks() override;
|
||||
std::optional<Webhook> GetWebhook(uint32_t id) override;
|
||||
void InsertWebhook(const Webhook& webhook) override;
|
||||
void UpdateWebhook(const Webhook& webhook) override;
|
||||
void DeleteWebhook(uint32_t id) override;
|
||||
void RecordWebhookResult(uint32_t id, int64_t time, int32_t status, const std::string& error) override;
|
||||
std::optional<std::string> GetDashboardState(const std::string& name) override;
|
||||
void SetDashboardState(const std::string& name, const std::string& value) override;
|
||||
void DeleteDashboardState(const std::string& name) override;
|
||||
std::string GetDashboardPreferences(uint32_t accountId) override;
|
||||
void SetDashboardPreferences(uint32_t accountId, const std::string& prefs) override;
|
||||
bool InsertEconomyFlag(const EconomyFlag& flag) override;
|
||||
nlohmann::json GetEconomyFlagsTable(uint32_t start, uint32_t length, int32_t status) override;
|
||||
void ReviewEconomyFlag(uint64_t id, eFlagStatus status, uint32_t reviewerAccountId, const std::string& note) override;
|
||||
uint32_t GetOpenEconomyFlagCount() override;
|
||||
std::vector<std::pair<LWOOBJID, int64_t>> GetDailyIncome(uint32_t day) override;
|
||||
std::map<LOT, int64_t> GetItemCreationTotals(uint32_t fromDay, uint32_t toDay) override;
|
||||
uint32_t CompactEconomy(uint32_t cutoffDay, uint32_t mapCutoffDay) override;
|
||||
uint32_t PruneTransfers(int64_t beforeTime) override;
|
||||
uint32_t PruneLog(eLog log, int64_t beforeTime) override;
|
||||
void ReportConfigFromFile(const Setting& setting) override;
|
||||
std::vector<Setting> GetServerConfig(const std::vector<std::string>& files) override;
|
||||
void SetWebConfigValue(const std::string& file, const std::string& name, const std::optional<std::string>& value, bool webWins, const std::string& by) override;
|
||||
void DeleteServerConfig(const std::string& file, const std::string& name) override;
|
||||
uint64_t InsertSettingChange(const SettingChange& change) override;
|
||||
std::vector<SettingChange> GetSettingChanges(const std::string& file, const std::string& name, uint32_t start, uint32_t length, uint64_t& total) override;
|
||||
std::optional<SettingChange> GetSettingChange(uint64_t id) override;
|
||||
// IModeration
|
||||
void SetStrikeStep(uint64_t strikeId, const std::string& step, uint32_t count) override;
|
||||
std::vector<AppliedStrikeStep> GetAppliedStrikeSteps(uint32_t accountId, int64_t since) override;
|
||||
uint64_t InsertPlayerReport(const PlayerReport& report) override;
|
||||
std::vector<PlayerReport> GetPlayerReports(const PlayerReportQuery& query) override;
|
||||
uint32_t CountPlayerReports(const PlayerReportQuery& query) override;
|
||||
std::optional<PlayerReport> GetPlayerReport(uint64_t id) override;
|
||||
void SetPlayerReportStatus(uint64_t id, uint8_t status, const std::string& handledBy, const std::string& resolution, int64_t time) override;
|
||||
std::vector<ChatFilterWord> GetChatFilterWords() override;
|
||||
void SetChatFilterWord(const ChatFilterWord& word) override;
|
||||
bool DeleteChatFilterWord(const std::string& word) override;
|
||||
void RecordLoginAddress(uint32_t accountId, const std::string& address, int64_t time) override;
|
||||
std::vector<LinkedAccount> GetLinkedAccounts(uint32_t accountId) override;
|
||||
uint32_t CountLoginAddresses(uint32_t accountId) override;
|
||||
std::vector<SlashCommand> GetSlashCommands() override;
|
||||
void SetSlashCommand(const SlashCommand& command) override;
|
||||
void DeleteSlashCommand(const std::string& name) override;
|
||||
std::vector<TaskSettings> GetScheduledTasks() override;
|
||||
void SetScheduledTask(const std::string& name, const std::optional<std::string>& schedule, bool enabled, const std::string& by) override;
|
||||
void SetTaskLastScheduled(const std::string& name, int64_t time) override;
|
||||
void InsertTaskRun(const TaskRun& run) override;
|
||||
nlohmann::json GetTaskRunsTable(const std::string& task, uint32_t start, uint32_t length) override;
|
||||
std::optional<TaskRun> GetTaskRun(uint64_t id) override;
|
||||
std::vector<TaskRun> GetLatestTaskRuns() override;
|
||||
uint32_t PruneTaskRuns(int64_t beforeTime) override;
|
||||
void InsertCharacterSnapshot(const CharacterSnapshot& snapshot) override;
|
||||
std::vector<CharacterSnapshot> GetCharacterSnapshots(LWOOBJID characterId) override;
|
||||
std::optional<CharacterSnapshot> GetCharacterSnapshot(uint64_t id) override;
|
||||
std::map<LWOOBJID, std::string> GetLatestSnapshotHashes() override;
|
||||
uint32_t PruneCharacterSnapshots(int64_t beforeTime, uint32_t keep) override;
|
||||
void InsertAccountNote(const AccountNote& note) override;
|
||||
std::vector<AccountNote> GetAccountNotes(uint32_t accountId) override;
|
||||
std::optional<AccountNote> GetAccountNote(uint64_t id) override;
|
||||
void DeleteAccountNote(uint64_t id) override;
|
||||
uint64_t InsertStrike(const Strike& strike) override;
|
||||
std::vector<Strike> GetStrikes(uint32_t accountId) override;
|
||||
std::optional<Strike> GetStrike(uint64_t id) override;
|
||||
void RevokeStrike(uint64_t id, const std::string& revokedBy, const std::string& reason, int64_t time) override;
|
||||
uint32_t CountActiveStrikes(uint32_t accountId, int64_t since) override;
|
||||
uint32_t GetAccountWarningCount(uint32_t accountId) override;
|
||||
void SetAccountBan(uint32_t accountId, bool banned, int64_t expires, const std::string& reason) override;
|
||||
std::vector<uint32_t> LiftExpiredBans(int64_t now) override;
|
||||
void InsertHealthSample(const HealthSample& sample) override;
|
||||
std::vector<HealthSample> GetHealthSamples(int64_t from, int64_t to, int64_t bucketSeconds) override;
|
||||
uint32_t PruneHealthSamples(int64_t beforeTime) override;
|
||||
void InsertInstanceSamples(const std::vector<InstanceSample>& samples) override;
|
||||
std::vector<InstanceSample> GetInstanceSamples(int64_t from, int64_t to, int64_t bucketSeconds, uint32_t zoneId) override;
|
||||
void InsertPositionSamples(const std::vector<PositionSample>& samples) override;
|
||||
std::vector<PositionSample> GetPositionSamples(uint32_t zoneId, uint32_t instanceId, int64_t from, int64_t to, int64_t bucketSeconds, uint32_t limit) override;
|
||||
std::vector<PositionInstance> GetPositionInstances(uint32_t zoneId, int64_t from, int64_t to) override;
|
||||
uint32_t PrunePositionSamples(int64_t beforeTime) override;
|
||||
uint64_t InsertAiSuggestion(const AiSuggestion& suggestion) override;
|
||||
std::vector<AiSuggestion> GetAiSuggestions(const std::string& kind, int64_t itemId, uint32_t limit) override;
|
||||
std::optional<AiSuggestion> FindAiSuggestion(const std::string& kind, int64_t itemId, const std::string& fingerprint) override;
|
||||
AiUsage GetAiUsageSince(int64_t since) override;
|
||||
std::optional<EconomyFlagRow> GetEconomyFlagRow(uint64_t id) override;
|
||||
nlohmann::json GetMapCellsPerDay(uint32_t zoneId, std::optional<uint32_t> cloneId, uint8_t kind, uint32_t fromDay, uint32_t toDay, uint32_t limit) override;
|
||||
std::vector<ScheduledAnnouncement> GetScheduledAnnouncements() override;
|
||||
uint64_t InsertScheduledAnnouncement(const ScheduledAnnouncement& announcement) override;
|
||||
void UpdateScheduledAnnouncement(const ScheduledAnnouncement& announcement) override;
|
||||
void MarkAnnouncementSent(uint64_t id, int64_t time) override;
|
||||
void DeleteScheduledAnnouncement(uint64_t id) override;
|
||||
std::vector<ScheduledEvent> GetScheduledEvents() override;
|
||||
uint64_t InsertScheduledEvent(const ScheduledEvent& event) override;
|
||||
void UpdateScheduledEvent(const ScheduledEvent& event) override;
|
||||
void DeleteScheduledEvent(uint64_t id) override;
|
||||
std::vector<ZoneLimit> GetZoneLimits() override;
|
||||
void SetZoneLimit(const ZoneLimit& limit) override;
|
||||
void DeleteZoneLimit(uint32_t zoneId) override;
|
||||
// IFeaturedProperties
|
||||
std::vector<FeaturedSlot> GetFeaturedPropertySlots() override;
|
||||
void SetFeaturedPropertySlot(const FeaturedSlot& slot) override;
|
||||
FeaturedSettings GetFeaturedPropertiesSettings() override;
|
||||
void SetFeaturedPropertiesSettings(const FeaturedSettings& settings) override;
|
||||
|
||||
|
||||
// IMessageCaptures
|
||||
uint64_t InsertMessageCaptureSession(const MessageCaptureSession& session) override;
|
||||
void UpdateMessageCaptureSession(const MessageCaptureSession& session) override;
|
||||
void InsertMessageCaptureEntries(const std::vector<MessageCaptureRecord>& entries) override;
|
||||
std::optional<MessageCaptureSession> GetMessageCaptureSession(uint64_t id) override;
|
||||
std::vector<MessageCaptureSession> GetMessageCaptureSessions(const SessionQuery& query) override;
|
||||
uint64_t CountMessageCaptureSessions(const SessionQuery& query) override;
|
||||
std::vector<MessageCaptureRecord> GetMessageCaptureEntries(uint64_t sessionId, uint32_t afterSeq, uint32_t limit) override;
|
||||
void DeleteMessageCaptureSession(uint64_t id) override;
|
||||
|
||||
// ILiveOps
|
||||
std::vector<LiveEvent> GetLiveEvents(bool activeOnly, uint32_t limit) override;
|
||||
std::optional<LiveEvent> GetLiveEvent(uint64_t id) override;
|
||||
uint64_t InsertLiveEvent(const LiveEvent& event) override;
|
||||
bool EndLiveEvent(uint64_t id, eLiveEventState state, int64_t endedAt, const std::string& endedBy, const std::string& reason) override;
|
||||
std::vector<LiveEventInstance> GetLiveEventInstances(const std::vector<uint64_t>& eventIds) override;
|
||||
void SetLiveEventInstance(const LiveEventInstance& instance) override;
|
||||
void AddLiveEventScores(uint64_t eventId, const std::vector<std::pair<LWOOBJID, int64_t>>& scores, int64_t time) override;
|
||||
std::vector<LiveOpsScore> GetLiveEventScores(uint64_t eventId, uint32_t limit) override;
|
||||
std::vector<Challenge> GetChallenges(bool openOnly) override;
|
||||
std::optional<Challenge> GetChallenge(uint64_t id) override;
|
||||
uint64_t InsertChallenge(const Challenge& challenge) override;
|
||||
void UpdateChallenge(const Challenge& challenge) override;
|
||||
void DeleteChallenge(uint64_t id) override;
|
||||
void AddChallengeContributions(const std::vector<Contribution>& contributions, int64_t time) override;
|
||||
std::map<uint64_t, ChallengeTotal> GetChallengeTotals(const std::vector<uint64_t>& challengeIds) override;
|
||||
std::vector<LiveOpsScore> GetChallengeContributions(uint64_t challengeId, uint32_t limit) override;
|
||||
std::map<uint64_t, int64_t> GetCharacterContributions(LWOOBJID characterId) override;
|
||||
std::vector<Reward> GetChallengeRewards(uint64_t challengeId) override;
|
||||
void InsertChallengeReward(const Reward& reward) override;
|
||||
std::vector<Reward> GetUnclaimedChallengeCoins(LWOOBJID characterId) override;
|
||||
bool ClaimChallengeCoins(uint64_t challengeId, LWOOBJID characterId, int64_t time) override;
|
||||
nlohmann::json GetPropertiesOwnedBy(LWOOBJID characterId) override;
|
||||
nlohmann::json GetBugReportsBy(LWOOBJID characterId, uint32_t limit) override;
|
||||
nlohmann::json GetEconomyFlagsFor(LWOOBJID characterId, uint32_t limit) override;
|
||||
nlohmann::json GetFriendsOf(LWOOBJID characterId) override;
|
||||
nlohmann::json GetCheatDetectionsFor(uint32_t accountId, uint32_t limit) override;
|
||||
nlohmann::json GetAuditAbout(uint32_t accountId, uint32_t limit) override;
|
||||
nlohmann::json GetPropertyRecord(LWOOBJID propertyId) override;
|
||||
nlohmann::json GetPropertyModelRecords(LWOOBJID propertyId) override;
|
||||
uint64_t InsertChatMessage(const ChatMessage& message) override;
|
||||
std::vector<ChatMessage> GetChatMessages(const ChatQuery& query) override;
|
||||
uint64_t CountChatMessages(const ChatQuery& query) override;
|
||||
void InsertModerationDecision(const std::string& kind, int64_t subjectId, const std::string& subject, bool approved, const std::string& reason, int64_t time) override;
|
||||
nlohmann::json GetModerationDecisions(const std::string& kind, int64_t subjectId, uint32_t limit) override;
|
||||
Totp GetTotp(uint32_t accountId) override;
|
||||
void SetTotp(uint32_t accountId, const std::string& encryptedSecret, int64_t enabledAt) override;
|
||||
bool UseTotpStep(uint32_t accountId, int64_t step) override;
|
||||
void ReplaceRecoveryCodes(uint32_t accountId, const std::vector<std::string>& codeHashes) override;
|
||||
bool UseRecoveryCode(uint32_t accountId, const std::string& codeHash) override;
|
||||
uint32_t GetRecoveryCodesLeft(uint32_t accountId) override;
|
||||
nlohmann::json GetBugReportsAfter(uint32_t afterId, uint32_t limit) override;
|
||||
uint32_t GetMaxBugReportId() override;
|
||||
nlohmann::json GetTransfersForCharacters(const std::vector<LWOOBJID>& characterIds, uint32_t start, uint32_t length) override;
|
||||
nlohmann::json GetMapZones(eMapEvent kind, uint32_t fromDay, uint32_t toDay) override;
|
||||
nlohmann::json GetMapLots(uint32_t zone, std::optional<uint32_t> clone, eMapEvent kind, uint32_t fromDay, uint32_t toDay, uint32_t limit) override;
|
||||
nlohmann::json GetMapCells(uint32_t zone, std::optional<uint32_t> clone, eMapEvent kind, uint32_t fromDay, uint32_t toDay, LOT lot) override;
|
||||
nlohmann::json GetMapEventsPerDay(uint32_t fromDay, uint32_t toDay, const PlaceFilter& place) override;
|
||||
nlohmann::json GetPlayerStatsPerDay(uint32_t fromDay, uint32_t toDay, bool excludeStaff, const PlaceFilter& place) override;
|
||||
nlohmann::json GetPlayerStatsPerZone(uint32_t fromDay, uint32_t toDay, bool excludeStaff) override;
|
||||
nlohmann::json GetMapEventsByLot(eMapEvent kind, uint32_t fromDay, uint32_t toDay, const PlaceFilter& place, uint32_t limit) override;
|
||||
nlohmann::json GetCloneOwners(const std::vector<uint32_t>& clones) override;
|
||||
nlohmann::json GetCloneVisitors(uint32_t zoneId, uint32_t cloneId, int64_t from, int64_t to, uint32_t limit) override;
|
||||
uint32_t ApprovePreviouslyApprovedPetNames() override;
|
||||
std::vector<std::pair<LWOOBJID, std::string>> GetAllPetNames() override;
|
||||
void ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) override;
|
||||
void ForEachCharacterXmlContaining(const std::string& needle, const std::function<void(LWOOBJID, const std::string&)>& visit) override;
|
||||
uint32_t FixPropertyCloneIds() override;
|
||||
std::optional<IAccountEmails::EmailInfo> GetAccountEmail(const uint32_t accountId) override;
|
||||
void SetAccountEmail(const uint32_t accountId, const std::string_view email, const bool confirmed) override;
|
||||
std::optional<uint32_t> GetAccountIdByConfirmedEmail(const std::string_view email) override;
|
||||
void InsertAccountToken(const std::string_view tokenHash, const uint32_t accountId, const std::string_view purpose, const std::string_view data, const int64_t expiresAt) override;
|
||||
std::optional<IAccountEmails::AccountToken> ConsumeAccountToken(const std::string_view tokenHash, const std::string_view purpose) override;
|
||||
void DeleteAccountTokens(const uint32_t accountId, const std::string_view purpose) override;
|
||||
void DeleteExpiredAccountTokens() override;
|
||||
int64_t GetSessionsValidAfter(const uint32_t accountId) override;
|
||||
void SetSessionsValidAfter(const uint32_t accountId, const int64_t time) override;
|
||||
std::optional<LWOOBJID> GetModelPropertyId(const LWOOBJID modelID) override;
|
||||
std::optional<int32_t> GetRedeemablePlayKeyId(const std::string_view keyString) override;
|
||||
void SetAccountPlayKey(const uint32_t accountId, const int32_t playkeyId) override;
|
||||
void DeleteBugReport(const uint32_t id) override;
|
||||
uint32_t GetBugReportCount() override;
|
||||
void InsertCheatDetection(const IPlayerCheatDetections::Info& info) override;
|
||||
void InsertNewMail(const MailInfo& mail) override;
|
||||
void InsertNewUgcModel(
|
||||
@@ -110,13 +318,17 @@ public:
|
||||
void UpdateAccountUnmuteTime(const uint32_t accountId, const uint64_t timeToUnmute) override;
|
||||
void UpdateAccountBan(const uint32_t accountId, const bool banned) override;
|
||||
void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) override;
|
||||
void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) override;
|
||||
void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword, const eGameMasterLevel gmLevel) override;
|
||||
void SetMasterInfo(const IServers::MasterInfo& info) override;
|
||||
std::optional<uint64_t> GetCurrentPersistentId() override;
|
||||
IObjectIdTracker::Range GetPersistentIdRange() override;
|
||||
void InsertDefaultPersistentId() override;
|
||||
std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) override;
|
||||
std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) override;
|
||||
std::string GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
uint32_t GetPlayKeyCount() override;
|
||||
void CreatePlayKey(const std::string_view keyString, const uint32_t uses, const std::string_view notes = "") override;
|
||||
void SetPlayKeyActive(const int32_t playkeyId, const bool active) override;
|
||||
std::vector<IUgc::Model> GetUgcModels(const LWOOBJID& propertyId) override;
|
||||
void AddIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
void RemoveIgnore(const LWOOBJID playerId, const LWOOBJID ignoredPlayerId) override;
|
||||
@@ -137,15 +349,48 @@ public:
|
||||
std::optional<ILeaderboard::Score> GetPlayerScore(const LWOOBJID playerId, const uint32_t gameId) override;
|
||||
void IncrementNumWins(const LWOOBJID playerId, const uint32_t gameId) override;
|
||||
void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) override;
|
||||
std::map<uint32_t, uint32_t> GetLeaderboardSizes() override;
|
||||
void DeleteLeaderboardScore(const LWOOBJID playerId, const uint32_t gameId) override;
|
||||
void ResetLeaderboard(const uint32_t gameId) override;
|
||||
void InsertUgcBuild(const std::string& modules, const LWOOBJID bigId, const std::optional<LWOOBJID> characterId) override;
|
||||
void DeleteUgcBuild(const LWOOBJID bigId) override;
|
||||
uint32_t GetAccountCount() override;
|
||||
uint32_t GetCharacterCount() override;
|
||||
void RecordFailedAttempt(const uint32_t accountId) override;
|
||||
void ClearFailedAttempts(const uint32_t accountId) override;
|
||||
void SetLockout(const uint32_t accountId, const int64_t lockoutUntil) override;
|
||||
bool IsLockedOut(const uint32_t accountId) override;
|
||||
void SetAccountLocked(const uint32_t accountId, const bool locked) override;
|
||||
uint8_t GetFailedAttempts(const uint32_t accountId) override;
|
||||
nlohmann::json GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
nlohmann::json GetAccountById(uint32_t accountId) override;
|
||||
uint32_t CountActiveAccountsAtGmLevel(const uint8_t gmLevel, const uint32_t excludeAccountId) override;
|
||||
bool IsNameInUse(const std::string_view name) override;
|
||||
nlohmann::json GetCharacterById(const LWOOBJID charId) override;
|
||||
std::optional<IPropertyContents::Model> GetModel(const LWOOBJID modelID) override;
|
||||
std::optional<IUgc::Model> GetUgcModel(const LWOOBJID ugcId) override;
|
||||
std::optional<IProperty::Info> GetPropertyInfo(const LWOOBJID id) override;
|
||||
std::string GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true, bool pendingOnly = false) override;
|
||||
uint32_t GetPropertyCount() override;
|
||||
IProperty::ShowcaseResult GetShowcaseProperties(const IProperty::ShowcaseQuery& query) override;
|
||||
void ApproveProperty(const LWOOBJID propertyId) override;
|
||||
std::string GetActivityLogTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
uint32_t GetActivityLogCount() override;
|
||||
std::string GetCommandLogTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
std::string GetPetNamesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true, bool pendingOnly = false) override;
|
||||
void ApprovePetName(const int64_t id) override;
|
||||
void RejectPetName(const int64_t id) override;
|
||||
std::vector<LWOOBJID> GetPetsWithUnknownOwner() override;
|
||||
void SetPetOwner(const LWOOBJID petId, const LWOOBJID ownerId) override;
|
||||
nlohmann::json GetAccountCharacters(uint32_t accountId) override;
|
||||
void DeleteAccount(const uint32_t accountId) override;
|
||||
void InsertAuditLog(uint32_t accountId, const std::string_view accountName, const std::string_view action, const std::string_view description, uint32_t targetAccountId, LWOOBJID targetCharacterId) override;
|
||||
std::string GetAuditLogTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) override;
|
||||
sql::PreparedStatement* CreatePreppedStmt(const std::string& query);
|
||||
private:
|
||||
// Each instance has its own connection (Database::CreateConnection makes a second one for another thread)
|
||||
sql::Connection* m_Con = nullptr;
|
||||
|
||||
|
||||
// Generic query functions that can be used for any query.
|
||||
// Return type may be different depending on the query, so it is up to the caller to check the return type.
|
||||
@@ -287,6 +532,17 @@ inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::opti
|
||||
}
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::optional<std::string> param) {
|
||||
if (param) {
|
||||
LOG_DEBUG("%s", param.value().c_str());
|
||||
stmt->setString(index, param.value().c_str());
|
||||
} else {
|
||||
LOG_DEBUG("Null");
|
||||
stmt->setNull(index, sql::DataType::SQLNULL);
|
||||
}
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void SetParam(UniquePreppedStmtRef stmt, const int index, const std::optional<LWOOBJID> param) {
|
||||
if (param) {
|
||||
|
||||
45
dDatabase/GameDatabase/MySQL/Tables/AccountNotes.cpp
Normal file
45
dDatabase/GameDatabase/MySQL/Tables/AccountNotes.cpp
Normal file
@@ -0,0 +1,45 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
void MySQLDatabase::InsertAccountNote(const AccountNote& note) {
|
||||
ExecuteInsert("INSERT INTO account_notes (account_id, kind, text, actor, created_at) VALUES (?, ?, ?, ?, ?);", note.accountId, note.kind, note.text, note.actor, note.createdAt);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteAccountNote(uint64_t id) {
|
||||
ExecuteDelete("DELETE FROM account_notes WHERE id = ?;", static_cast<int64_t>(id));
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetAccountBan(uint32_t accountId, bool banned, int64_t expires, const std::string& reason) {
|
||||
ExecuteUpdate("UPDATE accounts SET banned = ?, ban_expires = ?, ban_reason = ? WHERE id = ?;", banned, banned ? expires : int64_t{ 0 }, banned ? reason : std::string(), accountId);
|
||||
}
|
||||
|
||||
std::vector<IAccountNotes::AccountNote> MySQLDatabase::GetAccountNotes(uint32_t accountId) {
|
||||
std::vector<AccountNote> notes;
|
||||
auto result = ExecuteSelect("SELECT * FROM account_notes WHERE account_id = ? ORDER BY id DESC;", accountId);
|
||||
while (result->next()) {
|
||||
notes.push_back({ result->getUInt64("id"), result->getUInt("account_id"), result->getString("kind").c_str(),
|
||||
result->getString("text").c_str(), result->getString("actor").c_str(), result->getInt64("created_at") });
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
std::optional<IAccountNotes::AccountNote> MySQLDatabase::GetAccountNote(uint64_t id) {
|
||||
auto result = ExecuteSelect("SELECT * FROM account_notes WHERE id = ?;", static_cast<int64_t>(id));
|
||||
if (!result->next()) return std::nullopt;
|
||||
return AccountNote{ result->getUInt64("id"), result->getUInt("account_id"), result->getString("kind").c_str(),
|
||||
result->getString("text").c_str(), result->getString("actor").c_str(), result->getInt64("created_at") };
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetAccountWarningCount(uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT COUNT(*) AS count FROM account_notes WHERE account_id = ? AND kind = 'warning';", accountId);
|
||||
return result->next() ? result->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> MySQLDatabase::LiftExpiredBans(int64_t now) {
|
||||
std::vector<uint32_t> ids;
|
||||
{
|
||||
auto result = ExecuteSelect("SELECT id FROM accounts WHERE banned = 1 AND ban_expires > 0 AND ban_expires <= ?;", now);
|
||||
while (result->next()) ids.push_back(result->getUInt("id"));
|
||||
}
|
||||
if (!ids.empty()) ExecuteUpdate("UPDATE accounts SET banned = 0, ban_expires = 0, ban_reason = NULL WHERE banned = 1 AND ban_expires > 0 AND ban_expires <= ?;", now);
|
||||
return ids;
|
||||
}
|
||||
40
dDatabase/GameDatabase/MySQL/Tables/AccountStrikes.cpp
Normal file
40
dDatabase/GameDatabase/MySQL/Tables/AccountStrikes.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
namespace {
|
||||
template<typename Result> IAccountStrikes::Strike ReadStrike(Result& result) {
|
||||
return { result->getUInt64("id"), result->getUInt("account_id"), result->getInt64("character_id"), result->getString("source").c_str(),
|
||||
result->getString("subject").c_str(), result->getString("reason").c_str(), result->getUInt("given_by_id"), result->getString("given_by").c_str(),
|
||||
result->getInt64("created_at"), result->getInt64("revoked_at"), result->getString("revoked_by").c_str(), result->getString("revoke_reason").c_str() };
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::InsertStrike(const Strike& strike) {
|
||||
ExecuteInsert("INSERT INTO account_strikes (account_id, character_id, source, subject, reason, given_by_id, given_by, created_at, revoke_reason) VALUES (?, ?, ?, ?, ?, ?, ?, ?, '');",
|
||||
strike.accountId, strike.characterId, strike.source, strike.subject, strike.reason, strike.givenById, strike.givenBy, strike.createdAt);
|
||||
auto result = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;");
|
||||
return result->next() ? result->getUInt64("id") : 0;
|
||||
}
|
||||
|
||||
std::vector<IAccountStrikes::Strike> MySQLDatabase::GetStrikes(uint32_t accountId) {
|
||||
std::vector<Strike> strikes;
|
||||
auto result = ExecuteSelect("SELECT * FROM account_strikes WHERE account_id = ? ORDER BY id DESC;", accountId);
|
||||
while (result->next()) strikes.push_back(ReadStrike(result));
|
||||
return strikes;
|
||||
}
|
||||
|
||||
std::optional<IAccountStrikes::Strike> MySQLDatabase::GetStrike(uint64_t id) {
|
||||
auto result = ExecuteSelect("SELECT * FROM account_strikes WHERE id = ?;", id);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return ReadStrike(result);
|
||||
}
|
||||
|
||||
void MySQLDatabase::RevokeStrike(uint64_t id, const std::string& revokedBy, const std::string& reason, int64_t time) {
|
||||
ExecuteUpdate("UPDATE account_strikes SET revoked_at = ?, revoked_by = ?, revoke_reason = ? WHERE id = ? AND revoked_at = 0;", time, revokedBy, reason, id);
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::CountActiveStrikes(uint32_t accountId, int64_t since) {
|
||||
auto result = accountId == 0
|
||||
? ExecuteSelect("SELECT COUNT(*) AS count FROM account_strikes WHERE revoked_at = 0 AND created_at >= ?;", since)
|
||||
: ExecuteSelect("SELECT COUNT(*) AS count FROM account_strikes WHERE account_id = ? AND revoked_at = 0 AND created_at >= ?;", accountId, since);
|
||||
return result->next() ? result->getUInt("count") : 0;
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
#include "eGameMasterLevel.h"
|
||||
#include "json.hpp"
|
||||
|
||||
std::optional<IAccounts::Info> MySQLDatabase::GetAccountInfo(const std::string_view username) {
|
||||
auto result = ExecuteSelect("SELECT id, password, banned, locked, play_key_id, gm_level, mute_expire FROM accounts WHERE name = ? LIMIT 1;", username);
|
||||
auto result = ExecuteSelect("SELECT id, password, banned, locked, play_key_id, gm_level, mute_expire, ban_expires, ban_reason FROM accounts WHERE name = ? LIMIT 1;", username);
|
||||
|
||||
if (!result->next()) {
|
||||
return std::nullopt;
|
||||
@@ -17,6 +19,8 @@ std::optional<IAccounts::Info> MySQLDatabase::GetAccountInfo(const std::string_v
|
||||
toReturn.locked = result->getBoolean("locked");
|
||||
toReturn.playKeyId = result->getUInt("play_key_id");
|
||||
toReturn.muteExpire = result->getUInt64("mute_expire");
|
||||
toReturn.banExpires = result->getInt64("ban_expires");
|
||||
toReturn.banReason = result->isNull("ban_reason") ? "" : result->getString("ban_reason").c_str();
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
@@ -33,8 +37,8 @@ void MySQLDatabase::UpdateAccountPassword(const uint32_t accountId, const std::s
|
||||
ExecuteUpdate("UPDATE accounts SET password = ? WHERE id = ?;", bcryptpassword, accountId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) {
|
||||
ExecuteInsert("INSERT INTO accounts (name, password, gm_level) VALUES (?, ?, ?);", username, bcryptpassword, static_cast<int32_t>(eGameMasterLevel::OPERATOR));
|
||||
void MySQLDatabase::InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword, const eGameMasterLevel gmLevel) {
|
||||
ExecuteInsert("INSERT INTO accounts (name, password, gm_level) VALUES (?, ?, ?);", username, bcryptpassword, static_cast<int32_t>(gmLevel));
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) {
|
||||
@@ -45,3 +49,175 @@ uint32_t MySQLDatabase::GetAccountCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM accounts;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::RecordFailedAttempt(const uint32_t accountId) {
|
||||
ExecuteUpdate("UPDATE accounts SET failed_attempts = failed_attempts + 1 WHERE id = ?;", accountId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::ClearFailedAttempts(const uint32_t accountId) {
|
||||
ExecuteUpdate("UPDATE accounts SET failed_attempts = 0, lockout_time = NULL, last_login = NOW() WHERE id = ?;", accountId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetLockout(const uint32_t accountId, const int64_t lockoutUntil) {
|
||||
ExecuteUpdate("UPDATE accounts SET lockout_time = FROM_UNIXTIME(NULLIF(?, 0)), failed_attempts = 0 WHERE id = ?;", lockoutUntil, accountId);
|
||||
}
|
||||
|
||||
bool MySQLDatabase::IsLockedOut(const uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT 1 AS locked FROM accounts WHERE id = ? AND lockout_time IS NOT NULL AND lockout_time > NOW();", accountId);
|
||||
return result->next();
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::CountActiveAccountsAtGmLevel(const uint8_t gmLevel, const uint32_t excludeAccountId) {
|
||||
auto result = ExecuteSelect("SELECT COUNT(*) AS count FROM accounts WHERE gm_level = ? AND id != ? AND banned = 0 AND locked = 0;", static_cast<int32_t>(gmLevel), excludeAccountId);
|
||||
return result->next() ? result->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetAccountLocked(const uint32_t accountId, const bool locked) {
|
||||
ExecuteUpdate("UPDATE accounts SET locked = ? WHERE id = ?;", locked, accountId);
|
||||
}
|
||||
|
||||
uint8_t MySQLDatabase::GetFailedAttempts(const uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT failed_attempts FROM accounts WHERE id = ?;", accountId);
|
||||
if (!result->next()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return result->getUInt("failed_attempts");
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// A number in the search box also matches IDs exactly (-1 never matches)
|
||||
const int64_t searchId = GeneralUtils::TryParse<int64_t>(std::string(search)).value_or(-1);
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at FROM accounts";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE (name LIKE CONCAT('%', ?, '%') OR id = ? OR id IN (SELECT account_id FROM charinfo WHERE name LIKE CONCAT('%', ?, '%')))";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "name"; break;
|
||||
case 2: orderColumnName = "banned"; break;
|
||||
case 3: orderColumnName = "locked"; break;
|
||||
case 4: orderColumnName = "gm_level"; break;
|
||||
case 5: orderColumnName = "mute_expire"; break;
|
||||
case 6: orderColumnName = "created_at"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM accounts;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM accounts WHERE (name LIKE CONCAT('%', ?, '%') OR id = ? OR id IN (SELECT account_id FROM charinfo WHERE name LIKE CONCAT('%', ?, '%')));";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search, searchId, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto result = !search.empty()
|
||||
? ExecuteSelect(mainQuery, search, searchId, search, start, length)
|
||||
: ExecuteSelect(mainQuery, start, length);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json accountsArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json account = {
|
||||
{"id", result->getUInt("id")},
|
||||
{"name", result->getString("name")},
|
||||
{"banned", result->getBoolean("banned")},
|
||||
{"locked", result->getBoolean("locked")},
|
||||
{"gm_level", result->getInt("gm_level")},
|
||||
{"mute_expire", result->getUInt64("mute_expire")},
|
||||
{"created_at", result->getString("created_at")}
|
||||
};
|
||||
accountsArray.push_back(account);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 1},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", accountsArray}
|
||||
};
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetAccountById(uint32_t accountId) {
|
||||
try {
|
||||
const std::string query = "SELECT id, name, banned, locked, gm_level, mute_expire, created_at, ban_expires, ban_reason FROM accounts WHERE id = ?;";
|
||||
auto result = ExecuteSelect(query, accountId);
|
||||
|
||||
if (!result->next()) {
|
||||
return nlohmann::json{{"error", "Account not found"}};
|
||||
}
|
||||
|
||||
nlohmann::json account = {
|
||||
{"id", result->getUInt("id")},
|
||||
{"name", result->getString("name")},
|
||||
{"banned", result->getBoolean("banned")},
|
||||
{"locked", result->getBoolean("locked")},
|
||||
{"ban_expires", result->getInt64("ban_expires")},
|
||||
{"ban_reason", result->isNull("ban_reason") ? std::string() : std::string(result->getString("ban_reason").c_str())},
|
||||
{"gm_level", result->getInt("gm_level")},
|
||||
{"mute_expire", result->getUInt64("mute_expire")},
|
||||
{"created_at", result->getString("created_at")}
|
||||
};
|
||||
|
||||
return account;
|
||||
} catch (const sql::SQLException& e) {
|
||||
LOG_DEBUG("SQL Error: %s", e.what());
|
||||
return nlohmann::json{{"error", "Database error"}};
|
||||
}
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteAccount(const uint32_t accountId) {
|
||||
// All or nothing: a failure part way leaves the account as it was
|
||||
DatabaseTransaction transaction(*this);
|
||||
std::vector<LWOOBJID> characters;
|
||||
{
|
||||
auto result = ExecuteSelect("SELECT id FROM charinfo WHERE account_id = ?;", accountId);
|
||||
while (result->next()) characters.push_back(result->getInt64("id"));
|
||||
}
|
||||
for (const auto characterId : characters) {
|
||||
DeleteCharacter(characterId);
|
||||
ExecuteDelete("DELETE FROM character_snapshots WHERE character_id = ?;", characterId);
|
||||
}
|
||||
// Rows about the account itself. Audit log, chat log and reports stay as the record of what happened.
|
||||
for (const auto* table : { "account_tokens", "account_recovery_codes", "account_notes", "account_strikes", "account_login_addresses",
|
||||
"dashboard_preferences", "accounts_rewardcodes", "player_cheat_detections" }) {
|
||||
ExecuteDelete(std::string("DELETE FROM ") + table + " WHERE account_id = ?;", accountId);
|
||||
}
|
||||
ExecuteDelete("DELETE FROM accounts WHERE id = ?;", accountId);
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetAccountCharacters(uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT id, name, last_login FROM charinfo WHERE account_id = ? ORDER BY last_login DESC;", accountId);
|
||||
nlohmann::json chars = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
chars.push_back({
|
||||
{"id", std::to_string(result->getInt64("id"))},
|
||||
{"name", result->getString("name")},
|
||||
{"last_login", result->getUInt64("last_login")}
|
||||
});
|
||||
}
|
||||
return chars;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,64 @@
|
||||
#include "MySQLDatabase.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "json.hpp"
|
||||
|
||||
void MySQLDatabase::UpdateActivityLog(const LWOOBJID characterId, const eActivityType activityType, const LWOMAPID mapId) {
|
||||
ExecuteInsert("INSERT INTO activity_log (character_id, activity, time, map_id) VALUES (?, ?, ?, ?);",
|
||||
characterId, static_cast<uint32_t>(activityType), static_cast<uint32_t>(time(NULL)), mapId);
|
||||
}
|
||||
|
||||
std::string MySQLDatabase::GetActivityLogTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// A number in the search box also matches IDs exactly (-1 never matches)
|
||||
const int64_t searchId = GeneralUtils::TryParse<int64_t>(std::string(search)).value_or(-1);
|
||||
std::string baseQuery = "SELECT a.id, a.character_id, c.name as character_name, a.activity, a.time, a.map_id FROM activity_log a LEFT JOIN charinfo c ON a.character_id = c.id";
|
||||
std::string whereClause;
|
||||
if (!search.empty()) whereClause = " WHERE (c.name LIKE CONCAT('%', ?, '%') OR a.character_id = ?)";
|
||||
|
||||
std::string orderColumnName = "a.id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "a.id"; break;
|
||||
case 1: orderColumnName = "c.name"; break;
|
||||
case 2: orderColumnName = "a.activity"; break;
|
||||
case 3: orderColumnName = "a.time"; break;
|
||||
case 4: orderColumnName = "a.map_id"; break;
|
||||
}
|
||||
// Rows with the same value (e.g. the same second) come in id order, the same on every database
|
||||
std::string orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC") + ", a.id" + (orderAsc ? " ASC" : " DESC");
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
// Without a name search or sort, page through the log first and join names for that page only; joining first
|
||||
// costs a character lookup for every skipped row, which made later pages of a big log slow
|
||||
if (search.empty() && orderColumn != 1) {
|
||||
mainQuery = "SELECT a.id, a.character_id, c.name as character_name, a.activity, a.time, a.map_id FROM (SELECT * FROM activity_log a" + orderClause + " LIMIT ?, ?) a "
|
||||
"LEFT JOIN charinfo c ON a.character_id = c.id" + orderClause + ";";
|
||||
}
|
||||
|
||||
auto totalCountResult = ExecuteSelect("SELECT COUNT(*) as count FROM activity_log;");
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
auto filteredCountResult = ExecuteSelect("SELECT COUNT(*) as count FROM activity_log a LEFT JOIN charinfo c ON a.character_id = c.id WHERE (c.name LIKE CONCAT('%', ?, '%') OR a.character_id = ?);", search, searchId);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
auto result = !search.empty()
|
||||
? ExecuteSelect(mainQuery, search, searchId, start, length)
|
||||
: ExecuteSelect(mainQuery, start, length);
|
||||
|
||||
nlohmann::json dataArray = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
dataArray.push_back({
|
||||
{"id", result->getUInt("id")},
|
||||
{"character_name", result->getString("character_name")},
|
||||
{"activity", result->getUInt("activity")},
|
||||
{"time", result->getUInt64("time")},
|
||||
{"map_id", result->getUInt("map_id")}
|
||||
});
|
||||
}
|
||||
return nlohmann::json({{"draw", 0}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", dataArray}}).dump();
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetActivityLogCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM activity_log;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
302
dDatabase/GameDatabase/MySQL/Tables/Admin.cpp
Normal file
302
dDatabase/GameDatabase/MySQL/Tables/Admin.cpp
Normal file
@@ -0,0 +1,302 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
namespace {
|
||||
std::string Text(PreparedStmtResultSet& result, const char* field) {
|
||||
return result->isNull(field) ? "" : std::string(result->getString(field).c_str());
|
||||
}
|
||||
|
||||
IDashboardAdmin::Webhook WebhookRow(PreparedStmtResultSet& result) {
|
||||
IDashboardAdmin::Webhook webhook;
|
||||
webhook.id = result->getUInt("id");
|
||||
webhook.name = Text(result, "name");
|
||||
webhook.url = Text(result, "url");
|
||||
webhook.format = Text(result, "format");
|
||||
webhook.events = Text(result, "events");
|
||||
webhook.secret = Text(result, "secret");
|
||||
webhook.enabled = result->getInt("enabled") != 0;
|
||||
webhook.createdAt = result->getInt64("created_at");
|
||||
webhook.lastSentAt = result->getInt64("last_sent_at");
|
||||
webhook.lastStatus = result->getInt("last_status");
|
||||
webhook.lastError = Text(result, "last_error");
|
||||
return webhook;
|
||||
}
|
||||
|
||||
// A ledger table: its key columns (besides day) and the value columns that are summed
|
||||
struct LedgerTable {
|
||||
const char* name;
|
||||
const char* keys;
|
||||
std::vector<const char*> values;
|
||||
};
|
||||
|
||||
const std::vector<LedgerTable> DAILY_TABLES{
|
||||
{ "economy_currency_daily", "character_id, source", { "gained", "spent" } },
|
||||
{ "economy_uscore_daily", "character_id, source", { "gained", "lost" } },
|
||||
{ "economy_items_daily", "lot, source, gm", { "created", "destroyed" } },
|
||||
{ "player_stats_daily", "zone, clone_id, stat, gm", { "amount" } },
|
||||
};
|
||||
const LedgerTable MAP_TABLE{ "map_events_daily", "zone, clone_id, kind, lot, cell_x, cell_z", { "events", "quantity" } };
|
||||
}
|
||||
|
||||
std::vector<IDashboardAdmin::Webhook> MySQLDatabase::GetWebhooks() {
|
||||
auto result = ExecuteSelect("SELECT * FROM dashboard_webhooks ORDER BY id;");
|
||||
std::vector<Webhook> webhooks;
|
||||
while (result->next()) webhooks.push_back(WebhookRow(result));
|
||||
return webhooks;
|
||||
}
|
||||
|
||||
std::optional<IDashboardAdmin::Webhook> MySQLDatabase::GetWebhook(uint32_t id) {
|
||||
auto result = ExecuteSelect("SELECT * FROM dashboard_webhooks WHERE id = ?;", id);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return WebhookRow(result);
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertWebhook(const Webhook& webhook) {
|
||||
ExecuteInsert("INSERT INTO dashboard_webhooks (name, url, format, events, secret, enabled, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);",
|
||||
webhook.name, webhook.url, webhook.format, webhook.events, webhook.secret, webhook.enabled, static_cast<int64_t>(std::time(nullptr)));
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdateWebhook(const Webhook& webhook) {
|
||||
ExecuteUpdate("UPDATE dashboard_webhooks SET name = ?, url = ?, format = ?, events = ?, secret = ?, enabled = ? WHERE id = ?;",
|
||||
webhook.name, webhook.url, webhook.format, webhook.events, webhook.secret, webhook.enabled, webhook.id);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteWebhook(uint32_t id) {
|
||||
ExecuteDelete("DELETE FROM dashboard_webhooks WHERE id = ?;", id);
|
||||
}
|
||||
|
||||
void MySQLDatabase::RecordWebhookResult(uint32_t id, int64_t time, int32_t status, const std::string& error) {
|
||||
ExecuteUpdate("UPDATE dashboard_webhooks SET last_sent_at = ?, last_status = ?, last_error = ? WHERE id = ?;", time, status, error, id);
|
||||
}
|
||||
|
||||
std::optional<std::string> MySQLDatabase::GetDashboardState(const std::string& name) {
|
||||
auto result = ExecuteSelect("SELECT value FROM dashboard_state WHERE name = ?;", name);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return Text(result, "value");
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetDashboardState(const std::string& name, const std::string& value) {
|
||||
ExecuteInsert("INSERT INTO dashboard_state (name, value) VALUES (?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value);", name, value);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteDashboardState(const std::string& name) {
|
||||
ExecuteDelete("DELETE FROM dashboard_state WHERE name = ?;", name);
|
||||
}
|
||||
|
||||
std::string MySQLDatabase::GetDashboardPreferences(uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT prefs FROM dashboard_preferences WHERE account_id = ?;", accountId);
|
||||
if (!result->next()) return "{}";
|
||||
return Text(result, "prefs");
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetDashboardPreferences(uint32_t accountId, const std::string& prefs) {
|
||||
ExecuteInsert("INSERT INTO dashboard_preferences (account_id, prefs) VALUES (?, ?) ON DUPLICATE KEY UPDATE prefs = VALUES(prefs);", accountId, prefs);
|
||||
}
|
||||
|
||||
bool MySQLDatabase::InsertEconomyFlag(const EconomyFlag& flag) {
|
||||
return ExecuteUpdate(
|
||||
"INSERT IGNORE INTO economy_flags (created_at, day, kind, character_id, lot, item_id, value, baseline, details) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
static_cast<int64_t>(std::time(nullptr)), flag.day, static_cast<uint32_t>(flag.kind), flag.characterId, flag.lot, flag.itemId,
|
||||
flag.value, flag.baseline, flag.details) > 0;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetEconomyFlagsTable(uint32_t start, uint32_t length, int32_t status) {
|
||||
const std::string where = status >= 0 ? " WHERE f.status = ?" : "";
|
||||
uint32_t count = 0;
|
||||
{
|
||||
auto result = status >= 0 ? ExecuteSelect("SELECT COUNT(*) AS count FROM economy_flags f" + where + ";", status)
|
||||
: ExecuteSelect("SELECT COUNT(*) AS count FROM economy_flags f;");
|
||||
if (result->next()) count = result->getUInt("count");
|
||||
}
|
||||
const std::string query = "SELECT f.*, c.name AS character_name, a.name AS reviewer_name FROM economy_flags f "
|
||||
"LEFT JOIN charinfo c ON c.id = f.character_id LEFT JOIN accounts a ON a.id = f.reviewed_by" + where + " ORDER BY f.id DESC LIMIT ? OFFSET ?;";
|
||||
auto result = status >= 0 ? ExecuteSelect(query, status, length, start) : ExecuteSelect(query, length, start);
|
||||
nlohmann::json data = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
data.push_back({
|
||||
{"id", result->getInt64("id")},
|
||||
{"created_at", result->getInt64("created_at")},
|
||||
{"day", result->getInt("day")},
|
||||
{"kind", result->getInt("kind")},
|
||||
{"character_id", std::to_string(result->getInt64("character_id"))},
|
||||
{"character_name", Text(result, "character_name")},
|
||||
{"lot", result->getInt("lot")},
|
||||
{"item_id", std::to_string(result->getInt64("item_id"))},
|
||||
{"value", result->getInt64("value")},
|
||||
{"baseline", result->getInt64("baseline")},
|
||||
{"details", Text(result, "details")},
|
||||
{"status", result->getInt("status")},
|
||||
{"reviewer_name", Text(result, "reviewer_name")},
|
||||
{"reviewed_at", result->getInt64("reviewed_at")},
|
||||
{"note", Text(result, "note")}
|
||||
});
|
||||
}
|
||||
return { {"draw", 0}, {"recordsTotal", count}, {"recordsFiltered", count}, {"data", data} };
|
||||
}
|
||||
|
||||
void MySQLDatabase::ReviewEconomyFlag(uint64_t id, eFlagStatus status, uint32_t reviewerAccountId, const std::string& note) {
|
||||
ExecuteUpdate("UPDATE economy_flags SET status = ?, reviewed_by = ?, reviewed_at = ?, note = ? WHERE id = ?;",
|
||||
static_cast<uint32_t>(status), reviewerAccountId, static_cast<int64_t>(std::time(nullptr)), note, id);
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetOpenEconomyFlagCount() {
|
||||
auto result = ExecuteSelect("SELECT COUNT(*) AS count FROM economy_flags WHERE status = 0;");
|
||||
return result->next() ? result->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
std::vector<std::pair<LWOOBJID, int64_t>> MySQLDatabase::GetDailyIncome(uint32_t day) {
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT f.character_id, SUM(f.gained) AS gained FROM economy_currency_daily f "
|
||||
"LEFT JOIN charinfo c ON c.id = f.character_id LEFT JOIN accounts a ON a.id = c.account_id "
|
||||
"WHERE f.day = ? AND f.source NOT IN (3, 6) AND COALESCE(a.gm_level, 0) < 3 GROUP BY f.character_id HAVING SUM(f.gained) > 0;", day);
|
||||
std::vector<std::pair<LWOOBJID, int64_t>> rows;
|
||||
while (result->next()) rows.emplace_back(result->getInt64("character_id"), result->getInt64("gained"));
|
||||
return rows;
|
||||
}
|
||||
|
||||
std::map<LOT, int64_t> MySQLDatabase::GetItemCreationTotals(uint32_t fromDay, uint32_t toDay) {
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT lot, SUM(created) AS created FROM economy_items_daily WHERE day BETWEEN ? AND ? AND gm = 0 GROUP BY lot;", fromDay, toDay);
|
||||
std::map<LOT, int64_t> totals;
|
||||
while (result->next()) totals[result->getInt("lot")] = result->getInt64("created");
|
||||
return totals;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::CompactEconomy(uint32_t cutoffDay, uint32_t mapCutoffDay) {
|
||||
uint32_t merged = 0;
|
||||
const auto compact = [&](const LedgerTable& table, uint32_t cutoff) {
|
||||
std::vector<uint32_t> days;
|
||||
{
|
||||
auto result = ExecuteSelect(std::string("SELECT DISTINCT day FROM ") + table.name + " WHERE day < ?;", cutoff);
|
||||
while (result->next()) days.push_back(result->getUInt("day"));
|
||||
}
|
||||
std::string values, selectValues, sums;
|
||||
for (const auto* value : table.values) {
|
||||
values += std::string(", ") + value;
|
||||
selectValues += std::string(", s.") + value;
|
||||
sums += std::string(sums.empty() ? "" : ", ") + value + " = " + table.name + "." + value + " + VALUES(" + value + ")";
|
||||
}
|
||||
std::string selectKeys;
|
||||
for (const auto& key : GeneralUtils::SplitString(table.keys, ',')) {
|
||||
auto trimmed = key;
|
||||
trimmed.erase(0, trimmed.find_first_not_of(' '));
|
||||
selectKeys += std::string(selectKeys.empty() ? "" : ", ") + "s." + trimmed;
|
||||
}
|
||||
for (const auto day : days) {
|
||||
const auto month = MonthStartDay(day);
|
||||
if (month == day) continue;
|
||||
// MySQL buffers a SELECT from the table being inserted into, so this is safe
|
||||
// One short transaction per day, so row locks are never held for the whole compaction
|
||||
DatabaseTransaction transaction(*this);
|
||||
ExecuteInsert(std::string("INSERT INTO ") + table.name + " (day, " + table.keys + values + ") SELECT ?, " + selectKeys + selectValues +
|
||||
" FROM " + table.name + " s WHERE s.day = ? ON DUPLICATE KEY UPDATE " + sums + ";", month, day);
|
||||
const auto deleted = static_cast<uint32_t>(ExecuteUpdate(std::string("DELETE FROM ") + table.name + " WHERE day = ?;", day));
|
||||
transaction.Commit();
|
||||
merged += deleted;
|
||||
}
|
||||
};
|
||||
for (const auto& table : DAILY_TABLES) compact(table, cutoffDay);
|
||||
compact(MAP_TABLE, mapCutoffDay);
|
||||
return merged;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::PruneTransfers(int64_t beforeTime) {
|
||||
return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM economy_transfers WHERE time < ?;", beforeTime));
|
||||
}
|
||||
|
||||
IDashboardAdmin::Totp MySQLDatabase::GetTotp(uint32_t accountId) {
|
||||
Totp totp;
|
||||
auto result = ExecuteSelect("SELECT totp_secret, totp_enabled_at, totp_last_step FROM accounts WHERE id = ?;", accountId);
|
||||
if (!result->next()) return totp;
|
||||
totp.encryptedSecret = Text(result, "totp_secret");
|
||||
totp.enabledAt = result->getInt64("totp_enabled_at");
|
||||
totp.lastStep = result->getInt64("totp_last_step");
|
||||
return totp;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetTotp(uint32_t accountId, const std::string& encryptedSecret, int64_t enabledAt) {
|
||||
ExecuteUpdate("UPDATE accounts SET totp_secret = ?, totp_enabled_at = ?, totp_last_step = 0 WHERE id = ?;", encryptedSecret, enabledAt, accountId);
|
||||
}
|
||||
|
||||
bool MySQLDatabase::UseTotpStep(uint32_t accountId, int64_t step) {
|
||||
return ExecuteUpdate("UPDATE accounts SET totp_last_step = ? WHERE id = ? AND totp_last_step < ?;", step, accountId, step) > 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::ReplaceRecoveryCodes(uint32_t accountId, const std::vector<std::string>& codeHashes) {
|
||||
ExecuteDelete("DELETE FROM account_recovery_codes WHERE account_id = ?;", accountId);
|
||||
for (const auto& hash : codeHashes) ExecuteInsert("INSERT INTO account_recovery_codes (account_id, code_hash) VALUES (?, ?);", accountId, hash);
|
||||
}
|
||||
|
||||
bool MySQLDatabase::UseRecoveryCode(uint32_t accountId, const std::string& codeHash) {
|
||||
return ExecuteUpdate("UPDATE account_recovery_codes SET used_at = ? WHERE account_id = ? AND code_hash = ? AND used_at = 0;",
|
||||
static_cast<int64_t>(std::time(nullptr)), accountId, codeHash) > 0;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetRecoveryCodesLeft(uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT COUNT(*) AS count FROM account_recovery_codes WHERE account_id = ? AND used_at = 0;", accountId);
|
||||
return result->next() ? result->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetBugReportsAfter(uint32_t afterId, uint32_t limit) {
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT b.id, b.body, b.client_version, b.other_player_id, b.selection, b.reporter_id, c.name AS reporter_name FROM bug_reports b "
|
||||
"LEFT JOIN charinfo c ON c.id = b.reporter_id WHERE b.id > ? ORDER BY b.id LIMIT ?;", afterId, limit);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({
|
||||
{"id", result->getInt("id")}, {"body", Text(result, "body")}, {"client_version", Text(result, "client_version")},
|
||||
{"other_player_id", Text(result, "other_player_id")}, {"selection", Text(result, "selection")},
|
||||
{"reporter_id", std::to_string(result->getInt64("reporter_id"))}, {"reporter_name", Text(result, "reporter_name")}
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetMaxBugReportId() {
|
||||
auto result = ExecuteSelect("SELECT COALESCE(MAX(id), 0) AS id FROM bug_reports;");
|
||||
return result->next() ? result->getUInt("id") : 0;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetTransfersForCharacters(const std::vector<LWOOBJID>& characterIds, uint32_t start, uint32_t length) {
|
||||
nlohmann::json empty{ {"draw", 0}, {"recordsTotal", 0}, {"recordsFiltered", 0}, {"data", nlohmann::json::array()} };
|
||||
if (characterIds.empty()) return empty;
|
||||
// Ids are numbers from the database, so listing them inline is safe
|
||||
std::string list;
|
||||
for (const auto id : characterIds) list += (list.empty() ? "" : ",") + std::to_string(id);
|
||||
const std::string where = " WHERE (t.from_character IN (" + list + ") OR t.to_character IN (" + list + ")) AND t.method <> " +
|
||||
std::to_string(static_cast<uint32_t>(IEconomyLedger::eTransferMethod::INVENTORY_MOVE));
|
||||
|
||||
uint32_t total = 0;
|
||||
{
|
||||
auto countResult = ExecuteSelect("SELECT COUNT(*) AS count FROM economy_transfers t" + where + ";");
|
||||
if (countResult->next()) total = countResult->getUInt("count");
|
||||
}
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT t.*, cf.name AS from_name, ct.name AS to_name FROM economy_transfers t "
|
||||
"LEFT JOIN charinfo cf ON cf.id = t.from_character LEFT JOIN charinfo ct ON ct.id = t.to_character" + where + " ORDER BY t.id DESC LIMIT ? OFFSET ?;",
|
||||
length, start);
|
||||
nlohmann::json data = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
data.push_back({
|
||||
{"time", result->getInt64("time")}, {"method", result->getInt("method")},
|
||||
{"item_id", std::to_string(result->getInt64("item_id"))}, {"lot", result->getInt("lot")},
|
||||
{"count", result->getInt("count")}, {"coins", result->getInt64("coins")},
|
||||
{"from_character", std::to_string(result->getInt64("from_character"))}, {"from_name", Text(result, "from_name")},
|
||||
{"to_character", std::to_string(result->getInt64("to_character"))}, {"to_name", Text(result, "to_name")}
|
||||
});
|
||||
}
|
||||
return { {"draw", 0}, {"recordsTotal", total}, {"recordsFiltered", total}, {"data", data} };
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::PruneLog(eLog log, int64_t beforeTime) {
|
||||
switch (log) {
|
||||
case eLog::ACTIVITY: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM activity_log WHERE time > 0 AND time < ?;", beforeTime));
|
||||
case eLog::COMMAND: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM command_log WHERE time > 0 AND time < ?;", beforeTime));
|
||||
case eLog::AUDIT: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM audit_log WHERE timestamp > 0 AND timestamp < ?;", beforeTime));
|
||||
case eLog::CHEAT_DETECTION: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM player_cheat_detections WHERE violation_time < FROM_UNIXTIME(?);", beforeTime));
|
||||
case eLog::CHAT: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM chat_log WHERE time > 0 AND time < ?;", beforeTime));
|
||||
case eLog::LOGIN_ADDRESS: return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM account_login_addresses WHERE last_seen < ?;", beforeTime));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
75
dDatabase/GameDatabase/MySQL/Tables/AiSuggestions.cpp
Normal file
75
dDatabase/GameDatabase/MySQL/Tables/AiSuggestions.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
namespace {
|
||||
std::string Text(PreparedStmtResultSet& result, const char* field) {
|
||||
return result->isNull(field) ? "" : std::string(result->getString(field).c_str());
|
||||
}
|
||||
|
||||
IAiSuggestions::AiSuggestion SuggestionRow(PreparedStmtResultSet& result) {
|
||||
IAiSuggestions::AiSuggestion row;
|
||||
row.id = result->getUInt64("id");
|
||||
row.kind = Text(result, "kind");
|
||||
row.itemId = result->getInt64("item_id");
|
||||
row.fingerprint = Text(result, "fingerprint");
|
||||
row.requestedById = result->getUInt("requested_by_id");
|
||||
row.requestedBy = Text(result, "requested_by");
|
||||
row.createdAt = result->getInt64("created_at");
|
||||
row.model = Text(result, "model");
|
||||
row.inputTokens = result->getUInt("input_tokens");
|
||||
row.outputTokens = result->getUInt("output_tokens");
|
||||
row.status = static_cast<IAiSuggestions::eAiStatus>(result->getInt("status"));
|
||||
row.suggestion = Text(result, "suggestion");
|
||||
row.error = Text(result, "error");
|
||||
row.context = Text(result, "context");
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::InsertAiSuggestion(const AiSuggestion& s) {
|
||||
ExecuteInsert("INSERT INTO ai_suggestions (kind, item_id, fingerprint, requested_by_id, requested_by, created_at, model, input_tokens, output_tokens, status, suggestion, error, context) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
s.kind, s.itemId, s.fingerprint, s.requestedById, s.requestedBy, s.createdAt, s.model, s.inputTokens, s.outputTokens,
|
||||
static_cast<uint32_t>(s.status), s.suggestion, s.error, s.context);
|
||||
auto result = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;");
|
||||
return result->next() ? result->getUInt64("id") : 0;
|
||||
}
|
||||
|
||||
std::vector<IAiSuggestions::AiSuggestion> MySQLDatabase::GetAiSuggestions(const std::string& kind, int64_t itemId, uint32_t limit) {
|
||||
auto result = ExecuteSelect("SELECT * FROM ai_suggestions WHERE kind = ? AND item_id = ? ORDER BY id DESC LIMIT ?;", kind, itemId, limit);
|
||||
std::vector<AiSuggestion> rows;
|
||||
while (result->next()) rows.push_back(SuggestionRow(result));
|
||||
return rows;
|
||||
}
|
||||
|
||||
std::optional<IAiSuggestions::AiSuggestion> MySQLDatabase::FindAiSuggestion(const std::string& kind, int64_t itemId, const std::string& fingerprint) {
|
||||
auto result = ExecuteSelect("SELECT * FROM ai_suggestions WHERE kind = ? AND item_id = ? AND fingerprint = ? AND status = 0 ORDER BY id DESC LIMIT 1;",
|
||||
kind, itemId, fingerprint);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return SuggestionRow(result);
|
||||
}
|
||||
|
||||
IAiSuggestions::AiUsage MySQLDatabase::GetAiUsageSince(int64_t since) {
|
||||
auto result = ExecuteSelect("SELECT COUNT(*) AS requests, COALESCE(SUM(input_tokens), 0) AS input, COALESCE(SUM(output_tokens), 0) AS output "
|
||||
"FROM ai_suggestions WHERE created_at >= ?;", since);
|
||||
if (!result->next()) return {};
|
||||
return { result->getUInt("requests"), result->getUInt64("input"), result->getUInt64("output") };
|
||||
}
|
||||
|
||||
std::optional<IAiSuggestions::EconomyFlagRow> MySQLDatabase::GetEconomyFlagRow(uint64_t id) {
|
||||
auto result = ExecuteSelect("SELECT * FROM economy_flags WHERE id = ?;", id);
|
||||
if (!result->next()) return std::nullopt;
|
||||
EconomyFlagRow row;
|
||||
row.id = result->getUInt64("id");
|
||||
row.createdAt = result->getInt64("created_at");
|
||||
row.day = result->getUInt("day");
|
||||
row.kind = static_cast<uint8_t>(result->getInt("kind"));
|
||||
row.characterId = result->getInt64("character_id");
|
||||
row.lot = result->getInt("lot");
|
||||
row.itemId = result->getInt64("item_id");
|
||||
row.value = result->getInt64("value");
|
||||
row.baseline = result->getInt64("baseline");
|
||||
row.details = Text(result, "details");
|
||||
row.status = static_cast<uint8_t>(result->getInt("status"));
|
||||
row.note = Text(result, "note");
|
||||
return row;
|
||||
}
|
||||
75
dDatabase/GameDatabase/MySQL/Tables/AuditLog.cpp
Normal file
75
dDatabase/GameDatabase/MySQL/Tables/AuditLog.cpp
Normal file
@@ -0,0 +1,75 @@
|
||||
#include "MySQLDatabase.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "json.hpp"
|
||||
#include <ctime>
|
||||
|
||||
void MySQLDatabase::InsertAuditLog(uint32_t accountId, const std::string_view accountName, const std::string_view action, const std::string_view description,
|
||||
uint32_t targetAccountId, LWOOBJID targetCharacterId) {
|
||||
ExecuteInsert("INSERT INTO audit_log (account_id, account_name, action, description, timestamp, target_account_id, target_character_id) VALUES (?, ?, ?, ?, ?, ?, ?);",
|
||||
accountId, accountName, action, description, static_cast<uint64_t>(std::time(nullptr)), targetAccountId, targetCharacterId);
|
||||
}
|
||||
|
||||
std::string MySQLDatabase::GetAuditLogTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// A number in the search box also matches IDs exactly (-1 never matches)
|
||||
const int64_t searchId = GeneralUtils::TryParse<int64_t>(std::string(search)).value_or(-1);
|
||||
std::string baseQuery = "SELECT id, account_id, account_name, action, description, timestamp, target_account_id, target_character_id, "
|
||||
"(SELECT name FROM accounts WHERE accounts.id = audit_log.target_account_id) AS target_account_name, "
|
||||
"(SELECT name FROM charinfo WHERE charinfo.id = audit_log.target_character_id) AS target_character_name FROM audit_log";
|
||||
std::string whereClause;
|
||||
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE (action LIKE CONCAT('%', ?, '%') OR account_name LIKE CONCAT('%', ?, '%') OR description LIKE CONCAT('%', ?, '%') OR account_id = ? OR target_account_id = ? OR target_character_id = ?)";
|
||||
}
|
||||
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "account_name"; break;
|
||||
case 2: orderColumnName = "action"; break;
|
||||
case 5: orderColumnName = "timestamp"; break; // columns 3 and 4 (about, description) don't sort
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
// Rows with the same value (e.g. the same second) come in id order, the same on every database
|
||||
std::string orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC") + ", id" + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
auto totalCountResult = ExecuteSelect("SELECT COUNT(*) as count FROM audit_log;");
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
auto filteredCountResult = ExecuteSelect("SELECT COUNT(*) as count FROM audit_log WHERE (action LIKE CONCAT('%', ?, '%') OR account_name LIKE CONCAT('%', ?, '%') OR description LIKE CONCAT('%', ?, '%') OR account_id = ? OR target_account_id = ? OR target_character_id = ?);", search, search, search, searchId, searchId, searchId);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
auto result = !search.empty()
|
||||
? ExecuteSelect(mainQuery, search, search, search, searchId, searchId, searchId, start, length)
|
||||
: ExecuteSelect(mainQuery, start, length);
|
||||
|
||||
nlohmann::json dataArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
dataArray.push_back({
|
||||
{"id", result->getUInt("id")},
|
||||
{"account_name", result->getString("account_name")},
|
||||
{"action", result->getString("action")},
|
||||
{"description", result->getString("description")},
|
||||
{"timestamp", result->getUInt64("timestamp")},
|
||||
{"target_account_id", result->getUInt("target_account_id")},
|
||||
{"target_account_name", std::string(result->isNull("target_account_name") ? "" : result->getString("target_account_name").c_str())},
|
||||
{"target_character_id", std::to_string(result->getInt64("target_character_id"))},
|
||||
{"target_character_name", std::string(result->isNull("target_character_name") ? "" : result->getString("target_character_name").c_str())}
|
||||
});
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", dataArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
@@ -4,3 +4,15 @@ void MySQLDatabase::InsertNewBugReport(const IBugReports::Info& info) {
|
||||
ExecuteInsert("INSERT INTO `bug_reports`(body, client_version, other_player_id, selection, reporter_id) VALUES (?, ?, ?, ?, ?)",
|
||||
info.body, info.clientVersion, info.otherPlayer, info.selection, info.characterId);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
void MySQLDatabase::DeleteBugReport(const uint32_t id) {
|
||||
ExecuteUpdate("DELETE FROM bug_reports WHERE id = ?;", id);
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetBugReportCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM bug_reports;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
set(DDATABASES_DATABASES_MYSQL_TABLES_SOURCES
|
||||
"Accounts.cpp"
|
||||
"AccountsRewardCodes.cpp"
|
||||
"AuditLog.cpp"
|
||||
"Dashboard.cpp"
|
||||
"Economy.cpp"
|
||||
"Admin.cpp"
|
||||
"ServerConfig.cpp"
|
||||
"SlashCommands.cpp"
|
||||
"ScheduledTasks.cpp"
|
||||
"CharacterSnapshots.cpp"
|
||||
"AccountNotes.cpp"
|
||||
"AccountStrikes.cpp"
|
||||
"Moderation.cpp"
|
||||
"ServerHealth.cpp"
|
||||
"PlayerPositions.cpp"
|
||||
"AiSuggestions.cpp"
|
||||
"ServerOperations.cpp"
|
||||
"LiveOps.cpp"
|
||||
"FeaturedProperties.cpp"
|
||||
"MessageCaptures.cpp"
|
||||
"ChatLog.cpp"
|
||||
"RelatedData.cpp"
|
||||
"ActivityLog.cpp"
|
||||
"Behaviors.cpp"
|
||||
"BugReports.cpp"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "MySQLDatabase.h"
|
||||
#include "json.hpp"
|
||||
|
||||
std::vector<std::string> MySQLDatabase::GetApprovedCharacterNames() {
|
||||
auto result = ExecuteSelect("SELECT name FROM charinfo;");
|
||||
@@ -52,6 +53,11 @@ std::vector<LWOOBJID> MySQLDatabase::GetAccountCharacterIds(const LWOOBJID accou
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetCharacterCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM charinfo;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertNewCharacter(const ICharInfo::Info info) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`) VALUES (?,?,?,?,?,?)",
|
||||
@@ -80,3 +86,23 @@ bool MySQLDatabase::IsNameInUse(const std::string_view name) {
|
||||
|
||||
return result->next();
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetCharacterById(const LWOOBJID charId) {
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT c.id, c.name, c.pending_name, c.needs_rename, c.account_id, c.last_login, c.prop_clone_id, c.permission_map, a.name as account_name "
|
||||
"FROM charinfo c LEFT JOIN accounts a ON c.account_id = a.id WHERE c.id = ? LIMIT 1;", charId);
|
||||
if (!result->next()) {
|
||||
return nlohmann::json({{"error", "Character not found"}});
|
||||
}
|
||||
return nlohmann::json({
|
||||
{"id", std::to_string(result->getInt64("id"))},
|
||||
{"name", result->getString("name")},
|
||||
{"pending_name", result->getString("pending_name")},
|
||||
{"needs_rename", result->getBoolean("needs_rename")},
|
||||
{"account_id", result->getUInt("account_id")},
|
||||
{"account_name", result->getString("account_name")},
|
||||
{"last_login", result->getUInt64("last_login")},
|
||||
{"prop_clone_id", result->getUInt64("prop_clone_id")},
|
||||
{"permission_map", result->getUInt("permission_map")}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,3 +17,75 @@ void MySQLDatabase::UpdateCharacterXml(const LWOOBJID charId, const std::string_
|
||||
void MySQLDatabase::InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) {
|
||||
ExecuteInsert("INSERT INTO `charxml` (`id`, `xml_data`) VALUES (?,?)", characterId, lxfml);
|
||||
}
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
#include "json.hpp"
|
||||
|
||||
std::string MySQLDatabase::GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// A number in the search box also matches IDs exactly (-1 never matches)
|
||||
const int64_t searchId = GeneralUtils::TryParse<int64_t>(std::string(search)).value_or(-1);
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT c.id, c.name, c.account_id, c.last_login, a.name as account_name FROM charinfo c JOIN accounts a ON c.account_id = a.id";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Add search filter if provided
|
||||
if (!search.empty()) {
|
||||
whereClause = " WHERE (c.name LIKE CONCAT('%', ?, '%') OR c.id = ? OR a.name LIKE CONCAT('%', ?, '%'))";
|
||||
}
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "c.id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "c.id"; break;
|
||||
case 1: orderColumnName = "c.name"; break;
|
||||
case 2: orderColumnName = "a.name"; break;
|
||||
case 3: orderColumnName = "c.last_login"; break;
|
||||
default: orderColumnName = "c.id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC") + ", c.id";
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM charinfo;";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM charinfo c JOIN accounts a ON c.account_id = a.id WHERE (c.name LIKE CONCAT('%', ?, '%') OR c.id = ? OR a.name LIKE CONCAT('%', ?, '%'));";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search, searchId, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto result = !search.empty()
|
||||
? ExecuteSelect(mainQuery, search, searchId, search, start, length)
|
||||
: ExecuteSelect(mainQuery, start, length);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json charactersArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json character = {
|
||||
{"id", std::to_string(result->getUInt64("id"))},
|
||||
{"name", result->getString("name")},
|
||||
{"account_name", result->getString("account_name")},
|
||||
{"last_login", result->getUInt64("last_login")}
|
||||
};
|
||||
charactersArray.push_back(character);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", charactersArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
57
dDatabase/GameDatabase/MySQL/Tables/CharacterSnapshots.cpp
Normal file
57
dDatabase/GameDatabase/MySQL/Tables/CharacterSnapshots.cpp
Normal file
@@ -0,0 +1,57 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace {
|
||||
ICharacterSnapshots::CharacterSnapshot Row(PreparedStmtResultSet& result, bool withXml) {
|
||||
ICharacterSnapshots::CharacterSnapshot snapshot;
|
||||
snapshot.id = result->getUInt64("id");
|
||||
snapshot.characterId = result->getInt64("character_id");
|
||||
snapshot.takenAt = result->getInt64("taken_at");
|
||||
snapshot.reason = result->getString("reason").c_str();
|
||||
snapshot.actor = result->getString("actor").c_str();
|
||||
snapshot.size = result->getUInt("size");
|
||||
snapshot.hash = result->getString("hash").c_str();
|
||||
if (withXml) {
|
||||
std::unique_ptr<std::istream> blob(result->getBlob("xml"));
|
||||
std::stringstream data;
|
||||
data << blob->rdbuf();
|
||||
snapshot.compressed = data.str();
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertCharacterSnapshot(const CharacterSnapshot& snapshot) {
|
||||
std::istringstream xml(snapshot.compressed);
|
||||
ExecuteInsert("INSERT INTO character_snapshots (character_id, taken_at, reason, actor, size, hash, xml) VALUES (?, ?, ?, ?, ?, ?, ?);",
|
||||
snapshot.characterId, snapshot.takenAt, snapshot.reason, snapshot.actor, snapshot.size, snapshot.hash, static_cast<const std::istream*>(&xml));
|
||||
}
|
||||
|
||||
std::vector<ICharacterSnapshots::CharacterSnapshot> MySQLDatabase::GetCharacterSnapshots(LWOOBJID characterId) {
|
||||
std::vector<CharacterSnapshot> snapshots;
|
||||
auto result = ExecuteSelect("SELECT id, character_id, taken_at, reason, actor, size, hash FROM character_snapshots WHERE character_id = ? ORDER BY id DESC;", characterId);
|
||||
while (result->next()) snapshots.push_back(Row(result, false));
|
||||
return snapshots;
|
||||
}
|
||||
|
||||
std::optional<ICharacterSnapshots::CharacterSnapshot> MySQLDatabase::GetCharacterSnapshot(uint64_t id) {
|
||||
auto result = ExecuteSelect("SELECT * FROM character_snapshots WHERE id = ?;", static_cast<int64_t>(id));
|
||||
if (!result->next()) return std::nullopt;
|
||||
return Row(result, true);
|
||||
}
|
||||
|
||||
std::map<LWOOBJID, std::string> MySQLDatabase::GetLatestSnapshotHashes() {
|
||||
std::map<LWOOBJID, std::string> hashes;
|
||||
auto result = ExecuteSelect("SELECT s.character_id, s.hash FROM character_snapshots s "
|
||||
"JOIN (SELECT character_id, MAX(id) AS id FROM character_snapshots GROUP BY character_id) latest ON latest.id = s.id;");
|
||||
while (result->next()) hashes[result->getInt64("character_id")] = result->getString("hash").c_str();
|
||||
return hashes;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::PruneCharacterSnapshots(int64_t beforeTime, uint32_t keep) {
|
||||
// The extra derived table lets MySQL delete from the table it reads
|
||||
return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM character_snapshots WHERE taken_at < ? AND id IN (SELECT id FROM "
|
||||
"(SELECT id, ROW_NUMBER() OVER (PARTITION BY character_id ORDER BY id DESC) AS position FROM character_snapshots) ranked WHERE position > ?);",
|
||||
beforeTime, keep));
|
||||
}
|
||||
48
dDatabase/GameDatabase/MySQL/Tables/ChatLog.cpp
Normal file
48
dDatabase/GameDatabase/MySQL/Tables/ChatLog.cpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#include "MySQLDatabase.h"
|
||||
#include "GeneralUtils.h"
|
||||
|
||||
// The filters are all optional. Each binds the same parameters whether it is on or off, so both queries below take one
|
||||
// argument list. A filter that is off only compares its own parameters (worked out once, not per row); one that is on is
|
||||
// a plain comparison, so the character and account filters are answered from chat_log's indexes instead of a full scan.
|
||||
namespace {
|
||||
std::string Where(const IChatLog::ChatQuery& q) {
|
||||
std::string where = " WHERE id > ?";
|
||||
where += q.channel.empty() ? " AND (? = '' AND ? = '')" : " AND (? <> '' AND channel = ?)";
|
||||
where += q.includePrivate ? " AND (? = 1)" : " AND (? = 1 OR channel IN ('zone', 'web'))";
|
||||
where += q.characterId == 0 ? " AND (? = 0 AND ? = 0 AND ? = 0)" : " AND (? <> 0 AND (sender_id = ? OR recipient_id = ?))";
|
||||
where += q.accountId == 0 ? " AND (? = 0 AND ? = 0)" : " AND (? <> 0 AND account_id = ?)";
|
||||
where += q.zoneId == 0 ? " AND (? = 0 AND ? = 0)" : " AND (? <> 0 AND zone_id = ?)";
|
||||
where += q.instanceId < 0 ? " AND (? < 0 AND ? < 0)" : " AND (? >= 0 AND instance_id = ?)";
|
||||
where += q.since == 0 ? " AND (? = 0 AND ? = 0)" : " AND (? <> 0 AND time >= ?)";
|
||||
where += q.search.empty() ? " AND (? = '' AND ? = '' AND ? = '' AND ? = '')"
|
||||
: " AND (? <> '' AND (message LIKE CONCAT('%', ?, '%') ESCAPE '!' OR sender_name LIKE CONCAT('%', ?, '%') ESCAPE '!' OR recipient_name LIKE CONCAT('%', ?, '%') ESCAPE '!'))";
|
||||
where += q.blockedOnly ? " AND (? <> 0 AND blocked = 1)" : " AND (? = 0)";
|
||||
return where;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::InsertChatMessage(const ChatMessage& m) {
|
||||
ExecuteInsert("INSERT INTO chat_log (time, channel, sender_id, sender_name, account_id, recipient_id, recipient_name, zone_id, instance_id, clone_id, message, blocked) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
m.time, m.channel, m.senderId, m.senderName, m.accountId, m.recipientId, m.recipientName, m.zoneId, m.instanceId, m.cloneId, m.message, m.blocked);
|
||||
auto last = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;"); // this connection's insert, not another server's
|
||||
return last->next() ? last->getUInt64("id") : 0;
|
||||
}
|
||||
|
||||
std::vector<IChatLog::ChatMessage> MySQLDatabase::GetChatMessages(const ChatQuery& q) {
|
||||
std::vector<ChatMessage> messages;
|
||||
auto result = ExecuteSelect("SELECT * FROM chat_log" + Where(q) + " ORDER BY id " + std::string(q.newestFirst ? "DESC" : "ASC") + " LIMIT ? OFFSET ?;", static_cast<int64_t>(q.afterId), q.channel, q.channel, q.includePrivate, q.characterId, q.characterId, q.characterId, q.accountId, q.accountId, q.zoneId, q.zoneId, q.instanceId, q.instanceId, q.since, q.since, q.search, GeneralUtils::LikeEscape(q.search, '!'), GeneralUtils::LikeEscape(q.search, '!'), GeneralUtils::LikeEscape(q.search, '!'), q.blockedOnly, q.limit, q.offset);
|
||||
while (result->next()) {
|
||||
messages.push_back({ result->getUInt64("id"), result->getInt64("time"), std::string(result->getString("channel").c_str()),
|
||||
result->getInt64("sender_id"), std::string(result->getString("sender_name").c_str()), result->getUInt("account_id"),
|
||||
result->getInt64("recipient_id"), std::string(result->getString("recipient_name").c_str()), result->getUInt("zone_id"),
|
||||
result->getUInt("instance_id"), result->getUInt("clone_id"), std::string(result->getString("message").c_str()),
|
||||
result->getInt("blocked") != 0 });
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::CountChatMessages(const ChatQuery& q) {
|
||||
auto result = ExecuteSelect("SELECT COUNT(*) AS count FROM chat_log" + Where(q) + ";", static_cast<int64_t>(q.afterId), q.channel, q.channel, q.includePrivate, q.characterId, q.characterId, q.characterId, q.accountId, q.accountId, q.zoneId, q.zoneId, q.instanceId, q.instanceId, q.since, q.since, q.search, GeneralUtils::LikeEscape(q.search, '!'), GeneralUtils::LikeEscape(q.search, '!'), GeneralUtils::LikeEscape(q.search, '!'), q.blockedOnly);
|
||||
return result->next() ? result->getUInt64("count") : 0;
|
||||
}
|
||||
@@ -1,5 +1,58 @@
|
||||
#include "MySQLDatabase.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include <ctime>
|
||||
#include "json.hpp"
|
||||
|
||||
void MySQLDatabase::InsertSlashCommandUsage(const LWOOBJID characterId, const std::string_view command) {
|
||||
ExecuteInsert("INSERT INTO command_log (character_id, command) VALUES (?, ?);", characterId, command);
|
||||
ExecuteInsert("INSERT INTO command_log (character_id, command, time) VALUES (?, ?, ?);", characterId, command, static_cast<int64_t>(std::time(nullptr)));
|
||||
}
|
||||
|
||||
std::string MySQLDatabase::GetCommandLogTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// A number in the search box also matches IDs exactly (-1 never matches)
|
||||
const int64_t searchId = GeneralUtils::TryParse<int64_t>(std::string(search)).value_or(-1);
|
||||
std::string baseQuery = "SELECT cl.id, cl.character_id, c.name as character_name, cl.command, cl.time FROM command_log cl LEFT JOIN charinfo c ON cl.character_id = c.id";
|
||||
std::string whereClause;
|
||||
if (!search.empty()) whereClause = " WHERE (c.name LIKE CONCAT('%', ?, '%') OR cl.command LIKE CONCAT('%', ?, '%') OR cl.character_id = ?)";
|
||||
|
||||
std::string orderColumnName = "cl.id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "cl.id"; break;
|
||||
case 1: orderColumnName = "c.name"; break;
|
||||
case 2: orderColumnName = "cl.command"; break;
|
||||
case 3: orderColumnName = "cl.time"; break;
|
||||
}
|
||||
// Rows with the same value (e.g. the same second) come in id order, the same on every database
|
||||
std::string orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC") + ", cl.id" + (orderAsc ? " ASC" : " DESC");
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
// Without a name search or sort, page through the log first and join names for that page only; joining first
|
||||
// costs a character lookup for every skipped row, which made later pages of a big log slow
|
||||
if (search.empty() && orderColumn != 1) {
|
||||
mainQuery = "SELECT cl.id, cl.character_id, c.name as character_name, cl.command, cl.time FROM (SELECT * FROM command_log cl" + orderClause + " LIMIT ?, ?) cl "
|
||||
"LEFT JOIN charinfo c ON cl.character_id = c.id" + orderClause + ";";
|
||||
}
|
||||
|
||||
auto totalCountResult = ExecuteSelect("SELECT COUNT(*) as count FROM command_log;");
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
auto filteredCountResult = ExecuteSelect("SELECT COUNT(*) as count FROM command_log cl LEFT JOIN charinfo c ON cl.character_id = c.id WHERE (c.name LIKE CONCAT('%', ?, '%') OR cl.command LIKE CONCAT('%', ?, '%') OR cl.character_id = ?);", search, search, searchId);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
auto result = !search.empty()
|
||||
? ExecuteSelect(mainQuery, search, search, searchId, start, length)
|
||||
: ExecuteSelect(mainQuery, start, length);
|
||||
|
||||
nlohmann::json dataArray = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
dataArray.push_back({
|
||||
{"id", result->getUInt("id")},
|
||||
{"character_name", result->getString("character_name")},
|
||||
{"command", result->getString("command")},
|
||||
{"character_id", std::to_string(result->getInt64("character_id"))},
|
||||
{"time", result->getInt64("time")} // 0: logged before times were recorded
|
||||
});
|
||||
}
|
||||
return nlohmann::json({{"draw", 0}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", dataArray}}).dump();
|
||||
}
|
||||
|
||||
364
dDatabase/GameDatabase/MySQL/Tables/Dashboard.cpp
Normal file
364
dDatabase/GameDatabase/MySQL/Tables/Dashboard.cpp
Normal file
@@ -0,0 +1,364 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
#include "json.hpp"
|
||||
|
||||
// Queries backing the web dashboard's moderation features.
|
||||
|
||||
namespace {
|
||||
constexpr const char* SNAPSHOT_QUERY =
|
||||
"SELECT "
|
||||
"(SELECT COUNT(*) FROM accounts) AS accounts, "
|
||||
"(SELECT COALESCE(MAX(id), 0) FROM accounts) AS accounts_max_id, "
|
||||
"(SELECT COUNT(*) FROM charinfo) AS characters, "
|
||||
"(SELECT COUNT(*) FROM charinfo WHERE pending_name != '' AND needs_rename = 0) AS pending_names, "
|
||||
"(SELECT COUNT(*) FROM properties) AS properties, "
|
||||
"(SELECT COUNT(*) FROM properties WHERE mod_approved = 0 AND privacy_option = 2 AND rejection_reason = '') AS pending_properties, "
|
||||
"(SELECT COUNT(*) FROM play_keys) AS play_keys, "
|
||||
"(SELECT COUNT(*) FROM bug_reports) AS bug_reports, "
|
||||
"(SELECT COUNT(*) FROM bug_reports WHERE resolved_time IS NULL) AS unresolved_bug_reports, "
|
||||
"(SELECT COUNT(*) FROM pet_names) AS pet_names, "
|
||||
"(SELECT COUNT(*) FROM pet_names WHERE approved = 1) AS pending_pet_names, "
|
||||
"(SELECT COALESCE(MAX(id), 0) FROM activity_log) AS activity_log_max_id, "
|
||||
"(SELECT COALESCE(MAX(id), 0) FROM chat_log) AS chat_log_max_id, "
|
||||
"(SELECT COALESCE(MAX(id), 0) FROM command_log) AS command_log_max_id, "
|
||||
"(SELECT COALESCE(MAX(id), 0) FROM audit_log) AS audit_log_max_id, "
|
||||
"(SELECT COALESCE(MAX(id), 0) FROM mail) AS mail_max_id, "
|
||||
"(SELECT COUNT(*) FROM economy_flags WHERE status = 0) AS open_economy_flags, "
|
||||
"(SELECT COALESCE(MAX(id), 0) FROM economy_flags) AS economy_flags_max_id;";
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::string NullableString(PreparedStmtResultSet& result, const char* field) {
|
||||
return result->isNull(field) ? "" : result->getString(field).c_str();
|
||||
}
|
||||
}
|
||||
|
||||
std::string MySQLDatabase::GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc, int8_t resolvedFilter) {
|
||||
// A number in the search box also matches IDs exactly (-1 never matches)
|
||||
const int64_t searchId = GeneralUtils::TryParse<int64_t>(std::string(search)).value_or(-1);
|
||||
const std::string from = " FROM bug_reports b LEFT JOIN charinfo c ON c.id = b.reporter_id";
|
||||
|
||||
std::string resolvedCondition;
|
||||
if (resolvedFilter == 0) resolvedCondition = "b.resolved_time IS NULL";
|
||||
else if (resolvedFilter == 1) resolvedCondition = "b.resolved_time IS NOT NULL";
|
||||
|
||||
std::string where = resolvedCondition.empty() ? "" : " WHERE " + resolvedCondition;
|
||||
const std::string totalWhere = where;
|
||||
if (!search.empty()) {
|
||||
where += (where.empty() ? " WHERE " : " AND ");
|
||||
where += "(b.body LIKE CONCAT('%', ?, '%') OR b.other_player_id LIKE CONCAT('%', ?, '%') OR c.name LIKE CONCAT('%', ?, '%') OR b.id = ? OR b.reporter_id = ?)";
|
||||
}
|
||||
|
||||
std::string orderColumnName = "b.id";
|
||||
switch (orderColumn) {
|
||||
case 1: orderColumnName = "reporter_name"; break;
|
||||
case 2: orderColumnName = "b.client_version"; break;
|
||||
case 3: orderColumnName = "b.submitted"; break;
|
||||
case 4: orderColumnName = "b.resolved_time"; break;
|
||||
default: orderColumnName = "b.id"; break;
|
||||
}
|
||||
|
||||
auto totalResult = ExecuteSelect("SELECT COUNT(*) AS count" + from + totalWhere + ";");
|
||||
const uint32_t totalRecords = totalResult->next() ? totalResult->getUInt("count") : 0;
|
||||
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
auto filteredResult = ExecuteSelect("SELECT COUNT(*) AS count" + from + where + ";", search, search, search, searchId, searchId);
|
||||
filteredRecords = filteredResult->next() ? filteredResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
const std::string query =
|
||||
"SELECT b.id, b.body, b.client_version, b.other_player_id, b.selection, b.submitted, b.reporter_id, c.name AS reporter_name, b.resolved_time"
|
||||
+ from + where + " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC") + " LIMIT ?, ?;";
|
||||
auto result = !search.empty()
|
||||
? ExecuteSelect(query, search, search, search, searchId, searchId, start, length)
|
||||
: ExecuteSelect(query, start, length);
|
||||
|
||||
nlohmann::json data = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
data.push_back({
|
||||
{"id", result->getInt("id")},
|
||||
{"body", result->getString("body").c_str()},
|
||||
{"client_version", result->getString("client_version").c_str()},
|
||||
{"other_player_id", result->getString("other_player_id").c_str()},
|
||||
{"selection", result->getString("selection").c_str()},
|
||||
{"submitted", result->getString("submitted").c_str()},
|
||||
{"reporter_id", std::to_string(result->getInt64("reporter_id"))},
|
||||
{"reporter_name", NullableString(result, "reporter_name")},
|
||||
{"resolved", !result->isNull("resolved_time")}
|
||||
});
|
||||
}
|
||||
|
||||
return nlohmann::json({ {"draw", 0}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", data} }).dump();
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetBugReport(const uint32_t id) {
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT b.id, b.body, b.client_version, b.other_player_id, b.selection, b.submitted, b.reporter_id, c.name AS reporter_name, "
|
||||
"b.resolved_time, b.resolution, b.resoleved_by_id, a.name AS resolver_name "
|
||||
"FROM bug_reports b LEFT JOIN charinfo c ON c.id = b.reporter_id LEFT JOIN accounts a ON a.id = b.resoleved_by_id "
|
||||
"WHERE b.id = ? LIMIT 1;", id);
|
||||
if (!result->next()) return nlohmann::json{ {"error", "Bug report not found"} };
|
||||
|
||||
return nlohmann::json{
|
||||
{"id", result->getInt("id")},
|
||||
{"body", result->getString("body").c_str()},
|
||||
{"client_version", result->getString("client_version").c_str()},
|
||||
{"other_player_id", result->getString("other_player_id").c_str()},
|
||||
{"selection", result->getString("selection").c_str()},
|
||||
{"submitted", result->getString("submitted").c_str()},
|
||||
{"reporter_id", std::to_string(result->getInt64("reporter_id"))},
|
||||
{"reporter_name", NullableString(result, "reporter_name")},
|
||||
{"resolved", !result->isNull("resolved_time")},
|
||||
{"resolved_time", NullableString(result, "resolved_time")},
|
||||
{"resolution", NullableString(result, "resolution")},
|
||||
{"resolver_name", NullableString(result, "resolver_name")}
|
||||
};
|
||||
}
|
||||
|
||||
void MySQLDatabase::ResolveBugReport(const uint32_t id, const uint32_t resolverAccountId, const std::string_view resolution) {
|
||||
ExecuteUpdate("UPDATE bug_reports SET resolved_time = NOW(), resoleved_by_id = ?, resolution = ? WHERE id = ?;", resolverAccountId, resolution, id);
|
||||
}
|
||||
|
||||
std::string MySQLDatabase::GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc) {
|
||||
// A number in the search box also matches IDs exactly (-1 never matches)
|
||||
const int64_t searchId = GeneralUtils::TryParse<int64_t>(std::string(search)).value_or(-1);
|
||||
const std::string where = search.empty() ? "" : " WHERE (p.key_string LIKE CONCAT('%', ?, '%') OR p.notes LIKE CONCAT('%', ?, '%') OR p.id = ? OR p.id IN (SELECT play_key_id FROM accounts WHERE name LIKE CONCAT('%', ?, '%')))";
|
||||
|
||||
std::string orderColumnName = "p.id";
|
||||
switch (orderColumn) {
|
||||
case 1: orderColumnName = "p.key_string"; break;
|
||||
case 2: orderColumnName = "p.key_uses"; break;
|
||||
case 3: orderColumnName = "times_used"; break;
|
||||
case 4: orderColumnName = "p.created_at"; break;
|
||||
case 5: orderColumnName = "p.active"; break;
|
||||
default: orderColumnName = "p.id"; break;
|
||||
}
|
||||
|
||||
auto totalResult = ExecuteSelect("SELECT COUNT(*) AS count FROM play_keys;");
|
||||
const uint32_t totalRecords = totalResult->next() ? totalResult->getUInt("count") : 0;
|
||||
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
auto filteredResult = ExecuteSelect("SELECT COUNT(*) AS count FROM play_keys p" + where + ";", search, search, searchId, search);
|
||||
filteredRecords = filteredResult->next() ? filteredResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
const std::string query =
|
||||
"SELECT p.id, p.key_string, p.key_uses, p.created_at, p.active, p.notes, "
|
||||
"(SELECT COUNT(*) FROM accounts a WHERE a.play_key_id = p.id) AS times_used FROM play_keys p"
|
||||
+ where + " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC") + " LIMIT ?, ?;";
|
||||
auto result = !search.empty()
|
||||
? ExecuteSelect(query, search, search, searchId, search, start, length)
|
||||
: ExecuteSelect(query, start, length);
|
||||
|
||||
nlohmann::json data = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
data.push_back({
|
||||
{"id", result->getInt("id")},
|
||||
{"key_string", result->getString("key_string").c_str()},
|
||||
{"key_uses", result->getInt("key_uses")},
|
||||
{"times_used", result->getInt("times_used")},
|
||||
{"created_at", result->getString("created_at").c_str()},
|
||||
{"active", result->getInt("active") != 0},
|
||||
{"notes", NullableString(result, "notes")}
|
||||
});
|
||||
}
|
||||
|
||||
return nlohmann::json({ {"draw", 0}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", data} }).dump();
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetPlayKey(const int32_t playkeyId) {
|
||||
auto result = ExecuteSelect("SELECT id, key_string, key_uses, created_at, active, notes FROM play_keys WHERE id = ? LIMIT 1;", playkeyId);
|
||||
if (!result->next()) return nlohmann::json{ {"error", "Play key not found"} };
|
||||
|
||||
nlohmann::json key{
|
||||
{"id", result->getInt("id")},
|
||||
{"key_string", result->getString("key_string").c_str()},
|
||||
{"key_uses", result->getInt("key_uses")},
|
||||
{"created_at", result->getString("created_at").c_str()},
|
||||
{"active", result->getInt("active") != 0},
|
||||
{"notes", NullableString(result, "notes")}
|
||||
};
|
||||
|
||||
nlohmann::json accounts = nlohmann::json::array();
|
||||
auto accountResult = ExecuteSelect("SELECT id, name, created_at FROM accounts WHERE play_key_id = ? ORDER BY id;", playkeyId);
|
||||
while (accountResult->next()) {
|
||||
accounts.push_back({
|
||||
{"id", accountResult->getInt("id")},
|
||||
{"name", accountResult->getString("name").c_str()},
|
||||
{"created_at", accountResult->getString("created_at").c_str()}
|
||||
});
|
||||
}
|
||||
key["times_used"] = accounts.size();
|
||||
key["accounts"] = accounts;
|
||||
return key;
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdatePlayKey(const int32_t playkeyId, const uint32_t uses, const std::string_view notes, const bool active) {
|
||||
ExecuteUpdate("UPDATE play_keys SET key_uses = ?, notes = ?, active = ? WHERE id = ?;", uses, notes, active ? 1 : 0, playkeyId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeletePlayKey(const int32_t playkeyId) {
|
||||
ExecuteUpdate("UPDATE accounts SET play_key_id = NULL WHERE play_key_id = ?;", playkeyId);
|
||||
ExecuteDelete("DELETE FROM play_keys WHERE id = ?;", playkeyId);
|
||||
}
|
||||
|
||||
std::vector<std::pair<LWOOBJID, std::string>> MySQLDatabase::GetCharacterIdsAndNames() {
|
||||
auto result = ExecuteSelect("SELECT id, name FROM charinfo ORDER BY name;");
|
||||
std::vector<std::pair<LWOOBJID, std::string>> characters;
|
||||
while (result->next()) {
|
||||
characters.emplace_back(result->getInt64("id"), std::string(result->getString("name").c_str()));
|
||||
}
|
||||
return characters;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetPendingNamesTable(const uint32_t start, const uint32_t length) {
|
||||
auto countResult = ExecuteSelect("SELECT COUNT(*) AS count FROM charinfo WHERE pending_name != '' AND needs_rename = 0;");
|
||||
const uint32_t total = countResult->next() ? countResult->getUInt("count") : 0;
|
||||
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT c.id, c.name, c.pending_name, c.account_id, a.name AS account_name "
|
||||
"FROM charinfo c LEFT JOIN accounts a ON a.id = c.account_id "
|
||||
"WHERE c.pending_name != '' AND c.needs_rename = 0 ORDER BY c.id LIMIT ?, ?;", start, length);
|
||||
|
||||
nlohmann::json data = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
data.push_back({
|
||||
{"id", std::to_string(result->getInt64("id"))},
|
||||
{"name", result->getString("name").c_str()},
|
||||
{"pending_name", result->getString("pending_name").c_str()},
|
||||
{"account_id", result->getInt("account_id")},
|
||||
{"account_name", NullableString(result, "account_name")}
|
||||
});
|
||||
}
|
||||
return nlohmann::json{ {"draw", 0}, {"recordsTotal", total}, {"recordsFiltered", total}, {"data", data} };
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetCharacterPermissionMap(const LWOOBJID characterId, const uint64_t permissionMap) {
|
||||
ExecuteUpdate("UPDATE charinfo SET permission_map = ? WHERE id = ?;", permissionMap, characterId);
|
||||
}
|
||||
|
||||
IDashboardStats::Snapshot MySQLDatabase::GetDashboardSnapshot() {
|
||||
IDashboardStats::Snapshot snapshot;
|
||||
auto result = ExecuteSelect(SNAPSHOT_QUERY);
|
||||
if (!result->next()) return snapshot;
|
||||
snapshot.accounts = result->getUInt64("accounts");
|
||||
snapshot.accountsMaxId = result->getUInt64("accounts_max_id");
|
||||
snapshot.characters = result->getUInt64("characters");
|
||||
snapshot.pendingNames = result->getUInt64("pending_names");
|
||||
snapshot.properties = result->getUInt64("properties");
|
||||
snapshot.pendingProperties = result->getUInt64("pending_properties");
|
||||
snapshot.playKeys = result->getUInt64("play_keys");
|
||||
snapshot.bugReports = result->getUInt64("bug_reports");
|
||||
snapshot.unresolvedBugReports = result->getUInt64("unresolved_bug_reports");
|
||||
snapshot.petNames = result->getUInt64("pet_names");
|
||||
snapshot.pendingPetNames = result->getUInt64("pending_pet_names");
|
||||
snapshot.activityLogMaxId = result->getUInt64("activity_log_max_id");
|
||||
snapshot.chatLogMaxId = result->getUInt64("chat_log_max_id");
|
||||
snapshot.commandLogMaxId = result->getUInt64("command_log_max_id");
|
||||
snapshot.auditLogMaxId = result->getUInt64("audit_log_max_id");
|
||||
snapshot.mailMaxId = result->getUInt64("mail_max_id");
|
||||
snapshot.openEconomyFlags = result->getUInt64("open_economy_flags");
|
||||
snapshot.economyFlagsMaxId = result->getUInt64("economy_flags_max_id");
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
std::optional<LWOOBJID> MySQLDatabase::GetModelPropertyId(const LWOOBJID modelID) {
|
||||
auto result = ExecuteSelect("SELECT property_id FROM properties_contents WHERE id = ? LIMIT 1;", modelID);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return result->getInt64("property_id");
|
||||
}
|
||||
|
||||
std::optional<int32_t> MySQLDatabase::GetRedeemablePlayKeyId(const std::string_view keyString) {
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT p.id FROM play_keys p WHERE p.key_string = ? AND p.active = 1 "
|
||||
"AND (SELECT COUNT(*) FROM accounts a WHERE a.play_key_id = p.id) < p.key_uses LIMIT 1;", keyString);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return result->getInt("id");
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetAccountPlayKey(const uint32_t accountId, const int32_t playkeyId) {
|
||||
ExecuteUpdate("UPDATE accounts SET play_key_id = ? WHERE id = ?;", playkeyId, accountId);
|
||||
}
|
||||
|
||||
std::optional<IAccountEmails::EmailInfo> MySQLDatabase::GetAccountEmail(const uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT email, email_confirmed_at FROM accounts WHERE id = ? LIMIT 1;", accountId);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return IAccountEmails::EmailInfo{ result->isNull("email") ? "" : std::string(result->getString("email").c_str()), !result->isNull("email_confirmed_at") };
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetAccountEmail(const uint32_t accountId, const std::string_view email, const bool confirmed) {
|
||||
if (confirmed) ExecuteUpdate("UPDATE accounts SET email = ?, email_confirmed_at = CURRENT_TIMESTAMP WHERE id = ?;", email, accountId);
|
||||
else ExecuteUpdate("UPDATE accounts SET email = ?, email_confirmed_at = NULL WHERE id = ?;", email, accountId);
|
||||
}
|
||||
|
||||
std::optional<uint32_t> MySQLDatabase::GetAccountIdByConfirmedEmail(const std::string_view email) {
|
||||
auto result = ExecuteSelect("SELECT id FROM accounts WHERE LOWER(email) = LOWER(?) AND email_confirmed_at IS NOT NULL LIMIT 1;", email);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return static_cast<uint32_t>(result->getUInt("id"));
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertAccountToken(const std::string_view tokenHash, const uint32_t accountId, const std::string_view purpose, const std::string_view data, const int64_t expiresAt) {
|
||||
ExecuteInsert("INSERT INTO account_tokens (token_hash, account_id, purpose, data, expires_at) VALUES (?, ?, ?, ?, ?);", tokenHash, accountId, purpose, data, expiresAt);
|
||||
}
|
||||
|
||||
std::optional<IAccountEmails::AccountToken> MySQLDatabase::ConsumeAccountToken(const std::string_view tokenHash, const std::string_view purpose) {
|
||||
std::optional<IAccountEmails::AccountToken> token;
|
||||
{
|
||||
auto result = ExecuteSelect("SELECT account_id, data FROM account_tokens WHERE token_hash = ? AND purpose = ? AND expires_at > ? LIMIT 1;",
|
||||
tokenHash, purpose, static_cast<int64_t>(std::time(nullptr)));
|
||||
if (result->next()) token = IAccountEmails::AccountToken{ result->getUInt("account_id"), std::string(result->getString("data").c_str()) };
|
||||
}
|
||||
ExecuteDelete("DELETE FROM account_tokens WHERE token_hash = ?;", tokenHash);
|
||||
return token;
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteAccountTokens(const uint32_t accountId, const std::string_view purpose) {
|
||||
ExecuteDelete("DELETE FROM account_tokens WHERE account_id = ? AND purpose = ?;", accountId, purpose);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteExpiredAccountTokens() {
|
||||
ExecuteDelete("DELETE FROM account_tokens WHERE expires_at <= ?;", static_cast<int64_t>(std::time(nullptr)));
|
||||
}
|
||||
|
||||
int64_t MySQLDatabase::GetSessionsValidAfter(const uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT sessions_valid_after FROM accounts WHERE id = ? LIMIT 1;", accountId);
|
||||
return result->next() ? result->getInt64("sessions_valid_after") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetSessionsValidAfter(const uint32_t accountId, const int64_t time) {
|
||||
ExecuteUpdate("UPDATE accounts SET sessions_valid_after = ? WHERE id = ?;", time, accountId);
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::ApprovePreviouslyApprovedPetNames() {
|
||||
// The derived table is needed because MySQL can't select from the table being updated
|
||||
return static_cast<uint32_t>(ExecuteUpdate(
|
||||
"UPDATE pet_names SET approved = 2 WHERE approved = 1 AND pet_name IN "
|
||||
"(SELECT name FROM (SELECT pet_name AS name FROM pet_names WHERE approved = 2) AS approved_names);"));
|
||||
}
|
||||
|
||||
std::vector<std::pair<LWOOBJID, std::string>> MySQLDatabase::GetAllPetNames() {
|
||||
std::vector<std::pair<LWOOBJID, std::string>> pets;
|
||||
auto result = ExecuteSelect("SELECT id, pet_name FROM pet_names;");
|
||||
while (result->next()) pets.emplace_back(result->getInt64("id"), std::string(result->getString("pet_name").c_str()));
|
||||
return pets;
|
||||
}
|
||||
|
||||
void MySQLDatabase::ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) {
|
||||
auto result = ExecuteSelect("SELECT id, xml_data FROM charxml;");
|
||||
while (result->next()) visit(result->getInt64("id"), std::string(result->getString("xml_data").c_str()));
|
||||
}
|
||||
|
||||
void MySQLDatabase::ForEachCharacterXmlContaining(const std::string& needle, const std::function<void(LWOOBJID, const std::string&)>& visit) {
|
||||
auto result = ExecuteSelect("SELECT id, xml_data FROM charxml WHERE LOCATE(?, xml_data) > 0;", needle);
|
||||
while (result->next()) visit(result->getInt64("id"), std::string(result->getString("xml_data").c_str()));
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::FixPropertyCloneIds() {
|
||||
return static_cast<uint32_t>(ExecuteUpdate(
|
||||
"UPDATE properties SET clone_id = (SELECT c.prop_clone_id FROM charinfo c WHERE c.id = properties.owner_id) "
|
||||
"WHERE EXISTS (SELECT 1 FROM charinfo c WHERE c.id = properties.owner_id AND c.prop_clone_id IS NOT NULL "
|
||||
"AND (properties.clone_id IS NULL OR c.prop_clone_id != properties.clone_id));"));
|
||||
}
|
||||
288
dDatabase/GameDatabase/MySQL/Tables/Economy.cpp
Normal file
288
dDatabase/GameDatabase/MySQL/Tables/Economy.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
namespace {
|
||||
// Staff (GM 3+) are excluded from player reports, matching NexusDashboard. Deleted characters still count.
|
||||
constexpr const char* STAFF_FILTER =
|
||||
" LEFT JOIN charinfo c ON c.id = f.character_id LEFT JOIN accounts a ON a.id = c.account_id WHERE COALESCE(a.gm_level, 0) < 3 AND ";
|
||||
constexpr const char* NO_FILTER = " WHERE ";
|
||||
}
|
||||
|
||||
void MySQLDatabase::RecordEconomy(const std::vector<CurrencyFlow>& currency, const std::vector<UScoreFlow>& uscore,
|
||||
const std::vector<ItemFlow>& items, const std::vector<ItemTransfer>& transfers, const std::vector<MapEvent>& mapEvents,
|
||||
const std::vector<PlayerStat>& stats) {
|
||||
if (currency.empty() && uscore.empty() && items.empty() && transfers.empty() && mapEvents.empty() && stats.empty()) return;
|
||||
|
||||
DatabaseTransaction transaction(*this);
|
||||
for (const auto& flow : currency) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO economy_currency_daily (day, character_id, source, gained, spent) VALUES (?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE gained = gained + VALUES(gained), spent = spent + VALUES(spent);",
|
||||
flow.day, flow.characterId, flow.source, flow.gained, flow.spent);
|
||||
}
|
||||
for (const auto& flow : uscore) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO economy_uscore_daily (day, character_id, source, gained, lost) VALUES (?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE gained = gained + VALUES(gained), lost = lost + VALUES(lost);",
|
||||
flow.day, flow.characterId, flow.source, flow.gained, flow.lost);
|
||||
}
|
||||
for (const auto& flow : items) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO economy_items_daily (day, lot, source, gm, created, destroyed) VALUES (?, ?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE created = created + VALUES(created), destroyed = destroyed + VALUES(destroyed);",
|
||||
flow.day, flow.lot, flow.source, flow.gm ? 1 : 0, flow.created, flow.destroyed);
|
||||
}
|
||||
for (const auto& transfer : transfers) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO economy_transfers (time, method, item_id, new_item_id, lot, count, coins, from_character, to_character, zone, merged) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
transfer.time, static_cast<uint32_t>(transfer.method), transfer.itemId, transfer.newItemId, transfer.lot,
|
||||
transfer.count, transfer.coins, transfer.fromCharacter, transfer.toCharacter, transfer.zone, transfer.merged ? 1 : 0);
|
||||
}
|
||||
for (const auto& event : mapEvents) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO map_events_daily (day, zone, clone_id, kind, lot, cell_x, cell_z, events, quantity) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE events = events + VALUES(events), quantity = quantity + VALUES(quantity);",
|
||||
event.day, event.zone, event.clone, static_cast<uint32_t>(event.kind), event.lot, event.cellX, event.cellZ, event.events, event.quantity);
|
||||
}
|
||||
for (const auto& stat : stats) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO player_stats_daily (day, zone, clone_id, stat, gm, amount) VALUES (?, ?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount);",
|
||||
stat.day, stat.zone, stat.clone, stat.stat, stat.gm ? 1 : 0, stat.amount);
|
||||
}
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetCurrencyFlows(uint32_t fromDay, uint32_t toDay, bool excludeStaff) {
|
||||
auto result = ExecuteSelect(std::string("SELECT f.day, f.source, SUM(f.gained) AS gained, SUM(f.spent) AS spent FROM economy_currency_daily f") +
|
||||
(excludeStaff ? STAFF_FILTER : NO_FILTER) + "f.day BETWEEN ? AND ? GROUP BY f.day, f.source ORDER BY f.day;", fromDay, toDay);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"day", result->getInt("day")}, {"source", result->getInt("source")},
|
||||
{"gained", result->getInt64("gained")}, {"spent", result->getInt64("spent")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetUScoreFlows(uint32_t fromDay, uint32_t toDay, bool excludeStaff) {
|
||||
auto result = ExecuteSelect(std::string("SELECT f.day, f.source, SUM(f.gained) AS gained, SUM(f.lost) AS lost FROM economy_uscore_daily f") +
|
||||
(excludeStaff ? STAFF_FILTER : NO_FILTER) + "f.day BETWEEN ? AND ? GROUP BY f.day, f.source ORDER BY f.day;", fromDay, toDay);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"day", result->getInt("day")}, {"source", result->getInt("source")},
|
||||
{"gained", result->getInt64("gained")}, {"lost", result->getInt64("lost")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetItemFlows(uint32_t fromDay, uint32_t toDay, LOT lot, bool excludeStaff) {
|
||||
const std::string query = std::string("SELECT day, source, SUM(created) AS created, SUM(destroyed) AS destroyed FROM economy_items_daily "
|
||||
"WHERE day BETWEEN ? AND ?") + (excludeStaff ? " AND gm = 0" : "") + (lot > 0 ? " AND lot = ?" : "") + " GROUP BY day, source ORDER BY day;";
|
||||
auto result = lot > 0 ? ExecuteSelect(query, fromDay, toDay, lot) : ExecuteSelect(query, fromDay, toDay);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"day", result->getInt("day")}, {"source", result->getInt("source")},
|
||||
{"created", result->getInt64("created")}, {"destroyed", result->getInt64("destroyed")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetTopEarners(uint32_t fromDay, uint32_t toDay, uint32_t limit, bool excludeStaff) {
|
||||
// Trades and mail move coins between players rather than creating them, so they are left out of "income"
|
||||
auto result = ExecuteSelect(std::string(
|
||||
"SELECT f.character_id, COALESCE(c.name, '') AS name, SUM(f.gained) AS gained, SUM(f.spent) AS spent FROM economy_currency_daily f "
|
||||
"LEFT JOIN charinfo c ON c.id = f.character_id LEFT JOIN accounts a ON a.id = c.account_id "
|
||||
"WHERE f.day BETWEEN ? AND ? AND f.source NOT IN (3, 6)") + (excludeStaff ? " AND COALESCE(a.gm_level, 0) < 3" : "") +
|
||||
" GROUP BY f.character_id ORDER BY gained DESC LIMIT ?;", fromDay, toDay, limit);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"character_id", std::to_string(result->getInt64("character_id"))}, {"name", std::string(result->getString("name").c_str())},
|
||||
{"gained", result->getInt64("gained")}, {"spent", result->getInt64("spent")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetTopItems(uint32_t fromDay, uint32_t toDay, uint32_t limit, bool excludeStaff) {
|
||||
auto result = ExecuteSelect(std::string(
|
||||
"SELECT lot, SUM(created) AS created, SUM(destroyed) AS destroyed FROM economy_items_daily WHERE day BETWEEN ? AND ?") +
|
||||
(excludeStaff ? " AND gm = 0" : "") + " GROUP BY lot ORDER BY created DESC LIMIT ?;", fromDay, toDay, limit);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"lot", result->getInt("lot")}, {"created", result->getInt64("created")}, {"destroyed", result->getInt64("destroyed")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
namespace {
|
||||
nlohmann::json TransferRow(PreparedStmtResultSet& result) {
|
||||
return {
|
||||
{"id", result->getInt64("id")},
|
||||
{"time", result->getInt64("time")},
|
||||
{"method", result->getInt("method")},
|
||||
{"item_id", std::to_string(result->getInt64("item_id"))},
|
||||
{"new_item_id", std::to_string(result->getInt64("new_item_id"))},
|
||||
{"lot", result->getInt("lot")},
|
||||
{"count", result->getInt("count")},
|
||||
{"coins", result->getInt64("coins")},
|
||||
{"from_character", std::to_string(result->getInt64("from_character"))},
|
||||
{"from_name", result->isNull("from_name") ? "" : std::string(result->getString("from_name").c_str())},
|
||||
{"to_character", std::to_string(result->getInt64("to_character"))},
|
||||
{"to_name", result->isNull("to_name") ? "" : std::string(result->getString("to_name").c_str())},
|
||||
{"zone", result->getInt("zone")},
|
||||
{"merged", result->isNull("merged") ? nlohmann::json() : nlohmann::json(result->getInt("merged") != 0)}
|
||||
};
|
||||
}
|
||||
|
||||
constexpr const char* TRANSFER_SELECT =
|
||||
"SELECT t.*, cf.name AS from_name, ct.name AS to_name FROM economy_transfers t "
|
||||
"LEFT JOIN charinfo cf ON cf.id = t.from_character LEFT JOIN charinfo ct ON ct.id = t.to_character";
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetTransfers(uint32_t start, uint32_t length, LWOOBJID characterId, LOT lot) {
|
||||
std::string where = " WHERE t.method <> " + std::to_string(static_cast<uint32_t>(eTransferMethod::INVENTORY_MOVE));
|
||||
if (characterId != 0) where += " AND (t.from_character = ? OR t.to_character = ?)";
|
||||
if (lot > 0) where += " AND t.lot = ?";
|
||||
|
||||
const auto countQuery = "SELECT COUNT(*) AS count FROM economy_transfers t" + where + ";";
|
||||
const auto query = std::string(TRANSFER_SELECT) + where + " ORDER BY t.id DESC LIMIT ?, ?;";
|
||||
auto run = [&](const std::string& sql, auto... extra) {
|
||||
if (characterId != 0 && lot > 0) return ExecuteSelect(sql, characterId, characterId, lot, extra...);
|
||||
if (characterId != 0) return ExecuteSelect(sql, characterId, characterId, extra...);
|
||||
if (lot > 0) return ExecuteSelect(sql, lot, extra...);
|
||||
return ExecuteSelect(sql, extra...);
|
||||
};
|
||||
|
||||
auto countResult = run(countQuery);
|
||||
const auto total = countResult->next() ? countResult->getUInt("count") : 0;
|
||||
auto result = run(query, start, length);
|
||||
nlohmann::json data = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
data.push_back(TransferRow(result));
|
||||
}
|
||||
return { {"draw", 0}, {"recordsTotal", total}, {"recordsFiltered", total}, {"data", data} };
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetTransfersForItems(const std::vector<LWOOBJID>& itemIds) {
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
if (itemIds.empty()) return rows;
|
||||
// Ids are numbers, so listing them inline is safe
|
||||
std::string list;
|
||||
for (const auto id : itemIds) list += (list.empty() ? "" : ",") + std::to_string(id);
|
||||
auto result = ExecuteSelect(std::string(TRANSFER_SELECT) + " WHERE t.item_id IN (" + list + ") OR t.new_item_id IN (" + list + ") ORDER BY t.id;");
|
||||
while (result->next()) {
|
||||
rows.push_back(TransferRow(result));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
void MySQLDatabase::ForEachMailAttachment(const std::function<void(const MailAttachment&)>& visit) {
|
||||
auto result = ExecuteSelect("SELECT id, receiver_id, attachment_id, attachment_lot, attachment_count FROM mail WHERE attachment_lot > 0 AND attachment_count > 0;");
|
||||
while (result->next()) {
|
||||
visit({ result->getUInt64("id"), result->getInt64("receiver_id"), result->getInt64("attachment_id"),
|
||||
result->getInt("attachment_lot"), static_cast<uint32_t>(result->getInt("attachment_count")) });
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// " AND clone_id = n" for one clone, nothing for every instance
|
||||
std::string CloneSql(std::optional<uint32_t> clone) {
|
||||
return clone ? " AND clone_id = " + std::to_string(*clone) : "";
|
||||
}
|
||||
|
||||
std::string IdList(const std::vector<uint32_t>& ids) {
|
||||
std::string list;
|
||||
for (const auto id : ids) list += (list.empty() ? "" : ",") + std::to_string(id);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetMapZones(eMapEvent kind, uint32_t fromDay, uint32_t toDay) {
|
||||
auto result = ExecuteSelect("SELECT zone, clone_id, SUM(events) AS events, SUM(quantity) AS quantity FROM map_events_daily "
|
||||
"WHERE kind = ? AND day BETWEEN ? AND ? GROUP BY zone, clone_id ORDER BY events DESC, zone, clone_id;", static_cast<uint32_t>(kind), fromDay, toDay);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"zone", result->getInt("zone")}, {"clone", result->getUInt("clone_id")}, {"events", result->getInt64("events")}, {"quantity", result->getInt64("quantity")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetMapLots(uint32_t zone, std::optional<uint32_t> clone, eMapEvent kind, uint32_t fromDay, uint32_t toDay, uint32_t limit) {
|
||||
auto result = ExecuteSelect("SELECT lot, SUM(events) AS events, SUM(quantity) AS quantity FROM map_events_daily "
|
||||
"WHERE zone = ? AND kind = ? AND day BETWEEN ? AND ?" + CloneSql(clone) + " GROUP BY lot ORDER BY events DESC, lot LIMIT ?;", zone, static_cast<uint32_t>(kind), fromDay, toDay, limit);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"lot", result->getInt("lot")}, {"events", result->getInt64("events")}, {"quantity", result->getInt64("quantity")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetMapCells(uint32_t zone, std::optional<uint32_t> clone, eMapEvent kind, uint32_t fromDay, uint32_t toDay, LOT lot) {
|
||||
const std::string query = std::string("SELECT cell_x, cell_z, SUM(events) AS events, SUM(quantity) AS quantity FROM map_events_daily "
|
||||
"WHERE zone = ? AND kind = ? AND day BETWEEN ? AND ?") + CloneSql(clone) + (lot > 0 ? " AND lot = ?" : "") + " GROUP BY cell_x, cell_z;";
|
||||
auto result = lot > 0 ? ExecuteSelect(query, zone, static_cast<uint32_t>(kind), fromDay, toDay, lot) : ExecuteSelect(query, zone, static_cast<uint32_t>(kind), fromDay, toDay);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"x", result->getInt("cell_x")}, {"z", result->getInt("cell_z")}, {"events", result->getInt64("events")}, {"quantity", result->getInt64("quantity")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetMapEventsPerDay(uint32_t fromDay, uint32_t toDay, const PlaceFilter& place) {
|
||||
auto result = ExecuteSelect("SELECT day, kind, SUM(events) AS events, SUM(quantity) AS quantity FROM map_events_daily "
|
||||
"WHERE day BETWEEN ? AND ?" + place.Sql() + " GROUP BY day, kind ORDER BY day, kind;", fromDay, toDay);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"day", result->getInt("day")}, {"kind", result->getInt("kind")}, {"events", result->getInt64("events")}, {"quantity", result->getInt64("quantity")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetMapEventsByLot(eMapEvent kind, uint32_t fromDay, uint32_t toDay, const PlaceFilter& place, uint32_t limit) {
|
||||
auto result = ExecuteSelect("SELECT day, zone, clone_id, lot, SUM(events) AS events, SUM(quantity) AS quantity FROM map_events_daily "
|
||||
"WHERE kind = ? AND day BETWEEN ? AND ?" + place.Sql() + " GROUP BY day, zone, clone_id, lot ORDER BY day, zone, clone_id, lot LIMIT ?;",
|
||||
static_cast<uint32_t>(kind), fromDay, toDay, limit);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"day", result->getInt("day")}, {"zone", result->getInt("zone")}, {"clone", result->getUInt("clone_id")}, {"lot", result->getInt("lot")},
|
||||
{"events", result->getInt64("events")}, {"quantity", result->getInt64("quantity")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetPlayerStatsPerDay(uint32_t fromDay, uint32_t toDay, bool excludeStaff, const PlaceFilter& place) {
|
||||
auto result = ExecuteSelect(std::string("SELECT day, stat, SUM(amount) AS amount FROM player_stats_daily WHERE day BETWEEN ? AND ?") +
|
||||
(excludeStaff ? " AND gm = 0" : "") + place.Sql() + " GROUP BY day, stat ORDER BY day, stat;", fromDay, toDay);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"day", result->getInt("day")}, {"stat", result->getInt("stat")}, {"amount", result->getInt64("amount")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetPlayerStatsPerZone(uint32_t fromDay, uint32_t toDay, bool excludeStaff) {
|
||||
auto result = ExecuteSelect(std::string("SELECT zone, clone_id, stat, SUM(amount) AS amount FROM player_stats_daily WHERE day BETWEEN ? AND ?") +
|
||||
(excludeStaff ? " AND gm = 0" : "") + " GROUP BY zone, clone_id, stat ORDER BY amount DESC, zone, clone_id, stat;", fromDay, toDay);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"zone", result->getInt("zone")}, {"clone", result->getUInt("clone_id")}, {"stat", result->getInt("stat")}, {"amount", result->getInt64("amount")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetCloneOwners(const std::vector<uint32_t>& clones) {
|
||||
nlohmann::json owners = nlohmann::json::array();
|
||||
nlohmann::json properties = nlohmann::json::array();
|
||||
if (!clones.empty()) {
|
||||
const auto list = IdList(clones);
|
||||
auto result = ExecuteSelect("SELECT prop_clone_id, id, name FROM charinfo WHERE prop_clone_id IN (" + list + ") ORDER BY prop_clone_id;");
|
||||
while (result->next()) {
|
||||
owners.push_back({ {"clone", result->getUInt("prop_clone_id")}, {"character_id", std::to_string(result->getInt64("id"))}, {"name", std::string(result->getString("name").c_str())} });
|
||||
}
|
||||
auto props = ExecuteSelect("SELECT clone_id, zone_id, id, name, owner_id FROM properties WHERE clone_id IN (" + list + ") ORDER BY clone_id, zone_id;");
|
||||
while (props->next()) {
|
||||
properties.push_back({ {"clone", props->getUInt("clone_id")}, {"zone", props->getInt("zone_id")}, {"id", std::to_string(props->getInt64("id"))},
|
||||
{"name", std::string(props->getString("name").c_str())}, {"owner_id", std::to_string(props->getInt64("owner_id"))} });
|
||||
}
|
||||
}
|
||||
return { {"owners", owners}, {"properties", properties} };
|
||||
}
|
||||
36
dDatabase/GameDatabase/MySQL/Tables/FeaturedProperties.cpp
Normal file
36
dDatabase/GameDatabase/MySQL/Tables/FeaturedProperties.cpp
Normal file
@@ -0,0 +1,36 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
std::vector<IFeaturedProperties::FeaturedSlot> MySQLDatabase::GetFeaturedPropertySlots() {
|
||||
std::vector<FeaturedSlot> slots;
|
||||
auto result = ExecuteSelect("SELECT * FROM featured_properties ORDER BY template_id;");
|
||||
while (result->next()) {
|
||||
slots.push_back({ result->getUInt("template_id"), static_cast<uint8_t>(result->getUInt("mode")), result->getInt64("property_id"),
|
||||
result->getInt64("updated_at"), result->getString("updated_by").c_str(), result->getUInt("zone_id") });
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetFeaturedPropertySlot(const FeaturedSlot& slot) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO featured_properties (template_id, mode, property_id, updated_at, updated_by, zone_id) VALUES (?, ?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE mode = VALUES(mode), property_id = VALUES(property_id), updated_at = VALUES(updated_at), updated_by = VALUES(updated_by), zone_id = VALUES(zone_id);",
|
||||
slot.templateId, static_cast<uint32_t>(slot.mode), slot.propertyId, slot.updatedAt, slot.updatedBy, slot.zoneId);
|
||||
}
|
||||
|
||||
IFeaturedProperties::FeaturedSettings MySQLDatabase::GetFeaturedPropertiesSettings() {
|
||||
FeaturedSettings settings;
|
||||
auto result = ExecuteSelect("SELECT * FROM featured_properties_settings WHERE id = 1;");
|
||||
if (result->next()) {
|
||||
settings.fullAuto = result->getInt("full_auto") != 0;
|
||||
settings.updatedAt = result->getInt64("updated_at");
|
||||
settings.updatedBy = result->getString("updated_by").c_str();
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetFeaturedPropertiesSettings(const FeaturedSettings& settings) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO featured_properties_settings (id, full_auto, updated_at, updated_by) VALUES (1, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE full_auto = VALUES(full_auto), updated_at = VALUES(updated_at), updated_by = VALUES(updated_by);",
|
||||
settings.fullAuto ? 1 : 0, settings.updatedAt, settings.updatedBy);
|
||||
}
|
||||
@@ -89,3 +89,18 @@ std::optional<ILeaderboard::Score> MySQLDatabase::GetPlayerScore(const LWOOBJID
|
||||
void MySQLDatabase::IncrementNumWins(const LWOOBJID playerId, const uint32_t gameId) {
|
||||
ExecuteUpdate("UPDATE leaderboard SET numWins = numWins + 1 WHERE character_id = ? AND game_id = ?;", playerId, gameId);
|
||||
}
|
||||
|
||||
std::map<uint32_t, uint32_t> MySQLDatabase::GetLeaderboardSizes() {
|
||||
std::map<uint32_t, uint32_t> sizes;
|
||||
auto result = ExecuteSelect("SELECT game_id, COUNT(*) AS scores FROM leaderboard GROUP BY game_id;");
|
||||
while (result->next()) sizes[result->getUInt("game_id")] = result->getUInt("scores");
|
||||
return sizes;
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteLeaderboardScore(const LWOOBJID playerId, const uint32_t gameId) {
|
||||
ExecuteDelete("DELETE FROM leaderboard WHERE character_id = ? AND game_id = ?;", playerId, gameId);
|
||||
}
|
||||
|
||||
void MySQLDatabase::ResetLeaderboard(const uint32_t gameId) {
|
||||
ExecuteDelete("DELETE FROM leaderboard WHERE game_id = ?;", gameId);
|
||||
}
|
||||
|
||||
232
dDatabase/GameDatabase/MySQL/Tables/LiveOps.cpp
Normal file
232
dDatabase/GameDatabase/MySQL/Tables/LiveOps.cpp
Normal file
@@ -0,0 +1,232 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
|
||||
namespace {
|
||||
std::string JoinZones(const std::vector<uint32_t>& zones) {
|
||||
std::string text;
|
||||
for (const auto zone : zones) text += (text.empty() ? "" : ",") + std::to_string(zone);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> SplitZones(const std::string& text) {
|
||||
std::vector<uint32_t> zones;
|
||||
for (const auto& part : GeneralUtils::SplitString(text, ',')) {
|
||||
if (const auto zone = GeneralUtils::TryParse<uint32_t>(part)) zones.push_back(*zone);
|
||||
}
|
||||
return zones;
|
||||
}
|
||||
|
||||
std::string Text(PreparedStmtResultSet& result, const char* field) {
|
||||
return result->isNull(field) ? "" : std::string(result->getString(field).c_str());
|
||||
}
|
||||
|
||||
ILiveOps::LiveEvent ReadEvent(PreparedStmtResultSet& result) {
|
||||
ILiveOps::LiveEvent row;
|
||||
row.id = result->getUInt64("id");
|
||||
row.type = Text(result, "type");
|
||||
row.title = Text(result, "title");
|
||||
row.message = Text(result, "message");
|
||||
row.zones = SplitZones(Text(result, "zones"));
|
||||
row.instanceId = result->getInt("instance_id");
|
||||
row.config = Text(result, "config");
|
||||
row.startsAt = result->getInt64("starts_at");
|
||||
row.endsAt = result->getInt64("ends_at");
|
||||
row.state = static_cast<ILiveOps::eLiveEventState>(result->getInt("state"));
|
||||
row.endedAt = result->getInt64("ended_at");
|
||||
row.endReason = Text(result, "end_reason");
|
||||
row.createdBy = Text(result, "created_by");
|
||||
row.endedBy = Text(result, "ended_by");
|
||||
return row;
|
||||
}
|
||||
|
||||
ILiveOps::Challenge ReadChallenge(PreparedStmtResultSet& result) {
|
||||
ILiveOps::Challenge row;
|
||||
row.id = result->getUInt64("id");
|
||||
row.title = Text(result, "title");
|
||||
row.description = Text(result, "description");
|
||||
row.metricKind = static_cast<ILiveOps::eChallengeMetric>(result->getInt("metric_kind"));
|
||||
row.metric = result->getUInt("metric");
|
||||
row.lot = result->getInt("lot");
|
||||
row.zones = SplitZones(Text(result, "zones"));
|
||||
row.target = result->getInt64("target");
|
||||
row.startsAt = result->getInt64("starts_at");
|
||||
row.endsAt = result->getInt64("ends_at");
|
||||
row.includeStaff = result->getInt("include_staff") != 0;
|
||||
row.isPublic = result->getInt("is_public") != 0;
|
||||
row.rewardCoins = result->getInt64("reward_coins");
|
||||
row.rewardItems = Text(result, "reward_items");
|
||||
row.rewardMin = result->getInt64("reward_min");
|
||||
row.state = static_cast<ILiveOps::eChallengeState>(result->getInt("state"));
|
||||
row.milestone = static_cast<uint8_t>(result->getUInt("milestone"));
|
||||
row.completedAt = result->getInt64("completed_at");
|
||||
row.rewardedAt = result->getInt64("rewarded_at");
|
||||
row.rewardedCount = result->getUInt("rewarded_count");
|
||||
row.endAnnouncedAt = result->getInt64("end_announced_at");
|
||||
row.createdAt = result->getInt64("created_at");
|
||||
row.createdBy = Text(result, "created_by");
|
||||
row.updatedAt = result->getInt64("updated_at");
|
||||
row.updatedBy = Text(result, "updated_by");
|
||||
return row;
|
||||
}
|
||||
|
||||
ILiveOps::Reward ReadReward(PreparedStmtResultSet& result) {
|
||||
return { result->getUInt64("challenge_id"), result->getInt64("character_id"), result->getInt64("amount"), result->getInt64("coins"),
|
||||
result->getInt64("rewarded_at"), result->getInt64("claimed_at") };
|
||||
}
|
||||
|
||||
std::vector<ILiveOps::LiveOpsScore> ReadScores(PreparedStmtResultSet& result, const char* idField) {
|
||||
std::vector<ILiveOps::LiveOpsScore> scores;
|
||||
while (result->next()) {
|
||||
scores.push_back({ result->getUInt64(idField), result->getInt64("character_id"), Text(result, "name"), result->getInt64("amount") });
|
||||
}
|
||||
return scores;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ILiveOps::LiveEvent> MySQLDatabase::GetLiveEvents(bool activeOnly, uint32_t limit) {
|
||||
std::vector<LiveEvent> rows;
|
||||
auto result = ExecuteSelect(std::string("SELECT * FROM live_events") + (activeOnly ? " WHERE state = 0" : "") + " ORDER BY id DESC LIMIT ?;", limit);
|
||||
while (result->next()) rows.push_back(ReadEvent(result));
|
||||
return rows;
|
||||
}
|
||||
|
||||
std::optional<ILiveOps::LiveEvent> MySQLDatabase::GetLiveEvent(uint64_t id) {
|
||||
auto result = ExecuteSelect("SELECT * FROM live_events WHERE id = ?;", id);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return ReadEvent(result);
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::InsertLiveEvent(const LiveEvent& row) {
|
||||
ExecuteInsert("INSERT INTO live_events (type, title, message, zones, instance_id, config, starts_at, ends_at, state, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
row.type, row.title, row.message, JoinZones(row.zones), row.instanceId, row.config, row.startsAt, row.endsAt, static_cast<uint8_t>(row.state), row.createdBy);
|
||||
auto result = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;");
|
||||
return result->next() ? result->getUInt64("id") : 0;
|
||||
}
|
||||
|
||||
bool MySQLDatabase::EndLiveEvent(uint64_t id, eLiveEventState state, int64_t endedAt, const std::string& endedBy, const std::string& reason) {
|
||||
return ExecuteUpdate("UPDATE live_events SET state = ?, ended_at = ?, ended_by = ?, end_reason = ? WHERE id = ? AND state = 0;",
|
||||
static_cast<uint8_t>(state), endedAt, endedBy, reason, id) > 0;
|
||||
}
|
||||
|
||||
std::vector<ILiveOps::LiveEventInstance> MySQLDatabase::GetLiveEventInstances(const std::vector<uint64_t>& eventIds) {
|
||||
std::vector<LiveEventInstance> rows;
|
||||
for (const auto eventId : eventIds) {
|
||||
auto result = ExecuteSelect("SELECT * FROM live_event_instances WHERE event_id = ? ORDER BY zone_id, instance_id;", eventId);
|
||||
while (result->next()) {
|
||||
rows.push_back({ result->getUInt64("event_id"), result->getUInt("zone_id"), result->getUInt("instance_id"), Text(result, "status"), result->getInt64("updated_at") });
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetLiveEventInstance(const LiveEventInstance& row) {
|
||||
ExecuteInsert("INSERT INTO live_event_instances (event_id, zone_id, instance_id, status, updated_at) VALUES (?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE status = VALUES(status), updated_at = VALUES(updated_at);",
|
||||
row.eventId, row.zoneId, row.instanceId, row.status, row.updatedAt);
|
||||
}
|
||||
|
||||
void MySQLDatabase::AddLiveEventScores(uint64_t eventId, const std::vector<std::pair<LWOOBJID, int64_t>>& scores, int64_t time) {
|
||||
for (const auto& [characterId, amount] : scores) {
|
||||
ExecuteInsert("INSERT INTO live_event_scores (event_id, character_id, amount, updated_at) VALUES (?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount), updated_at = VALUES(updated_at);",
|
||||
eventId, characterId, amount, time);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ILiveOps::LiveOpsScore> MySQLDatabase::GetLiveEventScores(uint64_t eventId, uint32_t limit) {
|
||||
auto result = ExecuteSelect("SELECT s.event_id, s.character_id, c.name, s.amount FROM live_event_scores s LEFT JOIN charinfo c ON c.id = s.character_id "
|
||||
"WHERE s.event_id = ? ORDER BY s.amount DESC, s.updated_at LIMIT ?;", eventId, limit);
|
||||
return ReadScores(result, "event_id");
|
||||
}
|
||||
|
||||
std::vector<ILiveOps::Challenge> MySQLDatabase::GetChallenges(bool openOnly) {
|
||||
std::vector<Challenge> rows;
|
||||
auto result = ExecuteSelect(std::string("SELECT * FROM challenges") + (openOnly ? " WHERE state = 0" : "") + " ORDER BY id DESC;");
|
||||
while (result->next()) rows.push_back(ReadChallenge(result));
|
||||
return rows;
|
||||
}
|
||||
|
||||
std::optional<ILiveOps::Challenge> MySQLDatabase::GetChallenge(uint64_t id) {
|
||||
auto result = ExecuteSelect("SELECT * FROM challenges WHERE id = ?;", id);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return ReadChallenge(result);
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::InsertChallenge(const Challenge& row) {
|
||||
ExecuteInsert("INSERT INTO challenges (title, description, metric_kind, metric, lot, zones, target, starts_at, ends_at, include_staff, is_public, reward_coins, "
|
||||
"reward_items, reward_min, state, milestone, created_at, created_by, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
row.title, row.description, static_cast<uint8_t>(row.metricKind), row.metric, row.lot, JoinZones(row.zones), row.target, row.startsAt, row.endsAt,
|
||||
row.includeStaff, row.isPublic, row.rewardCoins, row.rewardItems, row.rewardMin, static_cast<uint8_t>(row.state), row.milestone,
|
||||
row.createdAt, row.createdBy, row.updatedAt, row.updatedBy);
|
||||
auto result = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;");
|
||||
return result->next() ? result->getUInt64("id") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdateChallenge(const Challenge& row) {
|
||||
ExecuteUpdate("UPDATE challenges SET title = ?, description = ?, metric_kind = ?, metric = ?, lot = ?, zones = ?, target = ?, starts_at = ?, ends_at = ?, "
|
||||
"include_staff = ?, is_public = ?, reward_coins = ?, reward_items = ?, reward_min = ?, state = ?, milestone = ?, completed_at = ?, rewarded_at = ?, "
|
||||
"rewarded_count = ?, end_announced_at = ?, updated_at = ?, updated_by = ? WHERE id = ?;",
|
||||
row.title, row.description, static_cast<uint8_t>(row.metricKind), row.metric, row.lot, JoinZones(row.zones), row.target, row.startsAt, row.endsAt,
|
||||
row.includeStaff, row.isPublic, row.rewardCoins, row.rewardItems, row.rewardMin, static_cast<uint8_t>(row.state), row.milestone, row.completedAt,
|
||||
row.rewardedAt, row.rewardedCount, row.endAnnouncedAt, row.updatedAt, row.updatedBy, row.id);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteChallenge(uint64_t id) {
|
||||
ExecuteDelete("DELETE FROM challenge_contributions WHERE challenge_id = ?;", id);
|
||||
ExecuteDelete("DELETE FROM challenge_rewards WHERE challenge_id = ?;", id);
|
||||
ExecuteDelete("DELETE FROM challenges WHERE id = ?;", id);
|
||||
}
|
||||
|
||||
void MySQLDatabase::AddChallengeContributions(const std::vector<Contribution>& contributions, int64_t time) {
|
||||
for (const auto& row : contributions) {
|
||||
ExecuteInsert("INSERT INTO challenge_contributions (challenge_id, character_id, amount, updated_at) VALUES (?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE amount = amount + VALUES(amount), updated_at = VALUES(updated_at);",
|
||||
row.challengeId, row.characterId, row.amount, time);
|
||||
}
|
||||
}
|
||||
|
||||
std::map<uint64_t, ILiveOps::ChallengeTotal> MySQLDatabase::GetChallengeTotals(const std::vector<uint64_t>& challengeIds) {
|
||||
std::map<uint64_t, ChallengeTotal> totals;
|
||||
for (const auto id : challengeIds) {
|
||||
auto result = ExecuteSelect("SELECT COALESCE(SUM(amount), 0) AS total, COUNT(*) AS contributors FROM challenge_contributions WHERE challenge_id = ? AND amount > 0;", id);
|
||||
if (result->next()) totals[id] = { result->getInt64("total"), result->getUInt("contributors") };
|
||||
}
|
||||
return totals;
|
||||
}
|
||||
|
||||
std::vector<ILiveOps::LiveOpsScore> MySQLDatabase::GetChallengeContributions(uint64_t challengeId, uint32_t limit) {
|
||||
auto result = ExecuteSelect("SELECT s.challenge_id, s.character_id, c.name, s.amount FROM challenge_contributions s LEFT JOIN charinfo c ON c.id = s.character_id "
|
||||
"WHERE s.challenge_id = ? AND s.amount > 0 ORDER BY s.amount DESC, s.updated_at LIMIT ?;", challengeId, limit == 0 ? UINT32_MAX : limit);
|
||||
return ReadScores(result, "challenge_id");
|
||||
}
|
||||
|
||||
std::map<uint64_t, int64_t> MySQLDatabase::GetCharacterContributions(LWOOBJID characterId) {
|
||||
std::map<uint64_t, int64_t> amounts;
|
||||
auto result = ExecuteSelect("SELECT challenge_id, amount FROM challenge_contributions WHERE character_id = ?;", characterId);
|
||||
while (result->next()) amounts[result->getUInt64("challenge_id")] = result->getInt64("amount");
|
||||
return amounts;
|
||||
}
|
||||
|
||||
std::vector<ILiveOps::Reward> MySQLDatabase::GetChallengeRewards(uint64_t challengeId) {
|
||||
std::vector<Reward> rows;
|
||||
auto result = ExecuteSelect("SELECT * FROM challenge_rewards WHERE challenge_id = ?;", challengeId);
|
||||
while (result->next()) rows.push_back(ReadReward(result));
|
||||
return rows;
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertChallengeReward(const Reward& row) {
|
||||
ExecuteInsert("INSERT IGNORE INTO challenge_rewards (challenge_id, character_id, amount, coins, rewarded_at, claimed_at) VALUES (?, ?, ?, ?, ?, ?);",
|
||||
row.challengeId, row.characterId, row.amount, row.coins, row.rewardedAt, row.claimedAt);
|
||||
}
|
||||
|
||||
std::vector<ILiveOps::Reward> MySQLDatabase::GetUnclaimedChallengeCoins(LWOOBJID characterId) {
|
||||
std::vector<Reward> rows;
|
||||
auto result = ExecuteSelect("SELECT * FROM challenge_rewards WHERE character_id = ? AND claimed_at = 0 AND coins > 0;", characterId);
|
||||
while (result->next()) rows.push_back(ReadReward(result));
|
||||
return rows;
|
||||
}
|
||||
|
||||
bool MySQLDatabase::ClaimChallengeCoins(uint64_t challengeId, LWOOBJID characterId, int64_t time) {
|
||||
return ExecuteUpdate("UPDATE challenge_rewards SET claimed_at = ? WHERE challenge_id = ? AND character_id = ? AND claimed_at = 0;", time, challengeId, characterId) > 0;
|
||||
}
|
||||
@@ -4,8 +4,8 @@
|
||||
void MySQLDatabase::InsertNewMail(const MailInfo& mail) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO `mail` "
|
||||
"(`sender_id`, `sender_name`, `receiver_id`, `receiver_name`, `time_sent`, `subject`, `body`, `attachment_id`, `attachment_lot`, `attachment_subkey`, `attachment_count`, `was_read`)"
|
||||
" VALUES (?,?,?,?,?,?,?,?,?,?,?,0)",
|
||||
"(`sender_id`, `sender_name`, `receiver_id`, `receiver_name`, `time_sent`, `subject`, `body`, `attachment_id`, `attachment_lot`, `attachment_subkey`, `attachment_count`, `attachment_config`, `was_read`)"
|
||||
" VALUES (?,?,?,?,?,?,?,?,?,?,?,?,0)",
|
||||
mail.senderId,
|
||||
mail.senderUsername,
|
||||
mail.receiverId,
|
||||
@@ -15,8 +15,9 @@ void MySQLDatabase::InsertNewMail(const MailInfo& mail) {
|
||||
mail.body,
|
||||
mail.itemID,
|
||||
mail.itemLOT,
|
||||
0,
|
||||
mail.itemCount);
|
||||
mail.itemSubkey,
|
||||
mail.itemCount,
|
||||
mail.itemConfig);
|
||||
}
|
||||
|
||||
std::vector<MailInfo> MySQLDatabase::GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) {
|
||||
@@ -34,9 +35,9 @@ std::vector<MailInfo> MySQLDatabase::GetMailForPlayer(const LWOOBJID characterId
|
||||
mail.subject = res->getString("subject").c_str();
|
||||
mail.body = res->getString("body").c_str();
|
||||
mail.senderUsername = res->getString("sender_name").c_str();
|
||||
mail.itemID = res->getUInt("attachment_id");
|
||||
mail.itemID = res->getInt64("attachment_id");
|
||||
mail.itemLOT = res->getInt("attachment_lot");
|
||||
mail.itemSubkey = res->getInt("attachment_subkey");
|
||||
mail.itemSubkey = res->getInt64("attachment_subkey");
|
||||
mail.itemCount = res->getInt("attachment_count");
|
||||
mail.timeSent = res->getUInt64("time_sent");
|
||||
mail.wasRead = res->getBoolean("was_read");
|
||||
@@ -48,15 +49,20 @@ std::vector<MailInfo> MySQLDatabase::GetMailForPlayer(const LWOOBJID characterId
|
||||
}
|
||||
|
||||
std::optional<MailInfo> MySQLDatabase::GetMail(const uint64_t mailId) {
|
||||
auto res = ExecuteSelect("SELECT attachment_lot, attachment_count, receiver_id FROM mail WHERE id=? LIMIT 1;", mailId);
|
||||
auto res = ExecuteSelect("SELECT sender_id, attachment_id, attachment_lot, attachment_subkey, attachment_count, attachment_config, receiver_id FROM mail WHERE id=? LIMIT 1;", mailId);
|
||||
|
||||
if (!res->next()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
MailInfo toReturn;
|
||||
toReturn.id = mailId;
|
||||
toReturn.senderId = res->getInt64("sender_id");
|
||||
toReturn.itemID = res->getInt64("attachment_id");
|
||||
toReturn.itemLOT = res->getInt("attachment_lot");
|
||||
toReturn.itemSubkey = res->getInt64("attachment_subkey");
|
||||
toReturn.itemCount = res->getInt("attachment_count");
|
||||
toReturn.itemConfig = res->isNull("attachment_config") ? "" : std::string(res->getString("attachment_config").c_str());
|
||||
toReturn.receiverId = res->getUInt64("receiver_id");
|
||||
|
||||
return toReturn;
|
||||
|
||||
140
dDatabase/GameDatabase/MySQL/Tables/MessageCaptures.cpp
Normal file
140
dDatabase/GameDatabase/MySQL/Tables/MessageCaptures.cpp
Normal file
@@ -0,0 +1,140 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
// The session filters bind the same parameters whether they are on or off (as in ChatLog.cpp), so the list and the
|
||||
// count take one argument list
|
||||
namespace {
|
||||
std::string Where(const IMessageCaptures::SessionQuery& q) {
|
||||
std::string where = " WHERE 1 = 1";
|
||||
where += q.characterId == 0 ? " AND (? = 0 AND ? = 0)" : " AND (? <> 0 AND character_id = ?)";
|
||||
where += q.accountId == 0 ? " AND (? = 0 AND ? = 0)" : " AND (? <> 0 AND account_id = ?)";
|
||||
where += q.startedBy.empty() ? " AND (? = '' AND ? = '')" : " AND (? <> '' AND LOWER(started_by) = LOWER(?))";
|
||||
where += q.since == 0 ? " AND (? = 0 AND ? = 0)" : " AND (? <> 0 AND started_at >= ?)";
|
||||
where += q.until == 0 ? " AND (? = 0 AND ? = 0)" : " AND (? <> 0 AND started_at < ?)";
|
||||
where += q.unfinishedOnly ? " AND (? <> 0 AND ended_at = 0)" : " AND (? = 0)";
|
||||
return where;
|
||||
}
|
||||
|
||||
std::string OrderBy(const IMessageCaptures::SessionQuery& q) {
|
||||
using eOrder = IMessageCaptures::eSessionOrder;
|
||||
const std::string direction = q.ascending ? " ASC" : " DESC";
|
||||
std::string column = "started_at";
|
||||
switch (q.order) {
|
||||
case eOrder::CHARACTER: column = "character_name"; break;
|
||||
case eOrder::STARTED_BY: column = "started_by"; break;
|
||||
case eOrder::MESSAGES: column = "message_count"; break;
|
||||
case eOrder::BYTES: column = "byte_count"; break;
|
||||
default: break;
|
||||
}
|
||||
// Ties in id order, the same on every database
|
||||
return " ORDER BY " + column + direction + ", id" + direction;
|
||||
}
|
||||
|
||||
IMessageCaptures::MessageCaptureSession Session(PreparedStmtResultSet& r) {
|
||||
IMessageCaptures::MessageCaptureSession s;
|
||||
s.id = r->getUInt64("id");
|
||||
s.characterId = r->getInt64("character_id");
|
||||
s.characterName = r->getString("character_name").c_str();
|
||||
s.accountId = r->getUInt("account_id");
|
||||
s.accountName = r->getString("account_name").c_str();
|
||||
s.startedById = r->getUInt("started_by_id");
|
||||
s.startedBy = r->getString("started_by").c_str();
|
||||
s.startedAt = r->getInt64("started_at");
|
||||
s.endsAt = r->getInt64("ends_at");
|
||||
s.endedAt = r->getInt64("ended_at");
|
||||
s.endReason = r->getString("end_reason").c_str();
|
||||
s.toServer = r->getInt("to_server") != 0;
|
||||
s.toClient = r->getInt("to_client") != 0;
|
||||
s.onlyMessages = r->getString("only_messages").c_str();
|
||||
s.skipMessages = r->getString("skip_messages").c_str();
|
||||
s.zoneId = r->getUInt("zone_id");
|
||||
s.instanceId = r->getUInt("instance_id");
|
||||
s.cloneId = r->getUInt("clone_id");
|
||||
s.zones = r->getString("zones").c_str();
|
||||
s.messageCount = r->getUInt64("message_count");
|
||||
s.byteCount = r->getUInt64("byte_count");
|
||||
s.dropped = r->getUInt64("dropped");
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::InsertMessageCaptureSession(const MessageCaptureSession& s) {
|
||||
ExecuteInsert("INSERT INTO message_capture_sessions (character_id, character_name, account_id, account_name, started_by_id, started_by, started_at, ends_at, "
|
||||
"ended_at, end_reason, to_server, to_client, only_messages, skip_messages, zone_id, instance_id, clone_id, zones, message_count, byte_count, dropped) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
s.characterId, s.characterName, s.accountId, s.accountName, s.startedById, s.startedBy, s.startedAt, s.endsAt, s.endedAt, s.endReason,
|
||||
s.toServer, s.toClient, s.onlyMessages, s.skipMessages, s.zoneId, s.instanceId, s.cloneId, s.zones, s.messageCount, s.byteCount, s.dropped);
|
||||
auto last = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;"); // this connection's insert
|
||||
return last->next() ? last->getUInt64("id") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdateMessageCaptureSession(const MessageCaptureSession& s) {
|
||||
ExecuteUpdate("UPDATE message_capture_sessions SET ends_at = ?, ended_at = ?, end_reason = ?, zone_id = ?, instance_id = ?, clone_id = ?, zones = ?, "
|
||||
"message_count = ?, byte_count = ?, dropped = ? WHERE id = ?;",
|
||||
s.endsAt, s.endedAt, s.endReason, s.zoneId, s.instanceId, s.cloneId, s.zones, s.messageCount, s.byteCount, s.dropped, s.id);
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertMessageCaptureEntries(const std::vector<MessageCaptureRecord>& entries) {
|
||||
if (entries.empty()) return;
|
||||
DatabaseTransaction transaction(*this);
|
||||
for (const auto& e : entries) {
|
||||
std::istringstream payload(e.payload);
|
||||
ExecuteInsert("INSERT INTO message_capture_entries (session_id, seq, time_ms, direction, message_id, object_id, bits, dropped_before, zone_id, instance_id, "
|
||||
"clone_id, payload, decoded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
e.sessionId, e.seq, e.timeMs, static_cast<uint32_t>(e.direction), static_cast<uint32_t>(e.messageId), e.objectId, e.bits, e.droppedBefore, e.zoneId, e.instanceId, e.cloneId,
|
||||
static_cast<const std::istream*>(&payload), e.decoded);
|
||||
}
|
||||
transaction.Commit();
|
||||
}
|
||||
|
||||
std::optional<IMessageCaptures::MessageCaptureSession> MySQLDatabase::GetMessageCaptureSession(uint64_t id) {
|
||||
auto result = ExecuteSelect("SELECT * FROM message_capture_sessions WHERE id = ?;", id);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return Session(result);
|
||||
}
|
||||
|
||||
std::vector<IMessageCaptures::MessageCaptureSession> MySQLDatabase::GetMessageCaptureSessions(const SessionQuery& q) {
|
||||
std::vector<MessageCaptureSession> sessions;
|
||||
auto result = ExecuteSelect("SELECT * FROM message_capture_sessions" + Where(q) + OrderBy(q) + " LIMIT ? OFFSET ?;",
|
||||
q.characterId, q.characterId, q.accountId, q.accountId, q.startedBy, q.startedBy, q.since, q.since, q.until, q.until, q.unfinishedOnly, q.limit, q.offset);
|
||||
while (result->next()) sessions.push_back(Session(result));
|
||||
return sessions;
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::CountMessageCaptureSessions(const SessionQuery& q) {
|
||||
auto result = ExecuteSelect("SELECT COUNT(*) AS count FROM message_capture_sessions" + Where(q) + ";",
|
||||
q.characterId, q.characterId, q.accountId, q.accountId, q.startedBy, q.startedBy, q.since, q.since, q.until, q.until, q.unfinishedOnly);
|
||||
return result->next() ? result->getUInt64("count") : 0;
|
||||
}
|
||||
|
||||
std::vector<IMessageCaptures::MessageCaptureRecord> MySQLDatabase::GetMessageCaptureEntries(uint64_t sessionId, uint32_t afterSeq, uint32_t limit) {
|
||||
std::vector<MessageCaptureRecord> entries;
|
||||
auto result = ExecuteSelect("SELECT * FROM message_capture_entries WHERE session_id = ? AND seq > ? ORDER BY seq LIMIT ?;", sessionId, afterSeq, limit);
|
||||
while (result->next()) {
|
||||
MessageCaptureRecord e;
|
||||
e.sessionId = result->getUInt64("session_id");
|
||||
e.seq = result->getUInt("seq");
|
||||
e.timeMs = result->getInt64("time_ms");
|
||||
e.direction = static_cast<uint8_t>(result->getUInt("direction"));
|
||||
e.messageId = static_cast<uint16_t>(result->getUInt("message_id"));
|
||||
e.objectId = result->getInt64("object_id");
|
||||
e.bits = result->getUInt("bits");
|
||||
e.droppedBefore = result->getUInt("dropped_before");
|
||||
e.zoneId = result->getUInt("zone_id");
|
||||
e.instanceId = result->getUInt("instance_id");
|
||||
e.cloneId = result->getUInt("clone_id");
|
||||
std::unique_ptr<std::istream> blob(result->getBlob("payload"));
|
||||
std::stringstream data;
|
||||
if (blob) data << blob->rdbuf();
|
||||
e.payload = data.str();
|
||||
e.decoded = result->getString("decoded").c_str();
|
||||
entries.push_back(std::move(e));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteMessageCaptureSession(uint64_t id) {
|
||||
ExecuteDelete("DELETE FROM message_capture_entries WHERE session_id = ?;", id);
|
||||
ExecuteDelete("DELETE FROM message_capture_sessions WHERE id = ?;", id);
|
||||
}
|
||||
112
dDatabase/GameDatabase/MySQL/Tables/Moderation.cpp
Normal file
112
dDatabase/GameDatabase/MySQL/Tables/Moderation.cpp
Normal file
@@ -0,0 +1,112 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include "eAccountLink.h"
|
||||
|
||||
namespace {
|
||||
// The filters are optional; each is written so an empty value turns it off, which keeps one prepared statement
|
||||
const std::string REPORTS_WHERE = " WHERE (? < 0 OR status = ?) AND (? = 0 OR target_account_id = ?)";
|
||||
|
||||
template<typename Result> IModeration::PlayerReport ReadReport(Result& r) {
|
||||
return { r->getUInt64("id"), r->getInt64("created_at"), std::string(r->getString("kind").c_str()), r->getInt64("reporter_id"),
|
||||
r->getUInt("reporter_account_id"), r->getInt64("object_id"), r->getInt("object_lot"), r->getInt64("target_character_id"),
|
||||
r->getUInt("target_account_id"), r->getInt64("property_id"), r->getUInt("zone_id"), r->getUInt("instance_id"), r->getUInt("clone_id"),
|
||||
std::string(r->getString("body").c_str()), static_cast<uint8_t>(r->getInt("status")), std::string(r->getString("handled_by").c_str()),
|
||||
r->getInt64("handled_at"), std::string(r->getString("resolution").c_str()) };
|
||||
}
|
||||
|
||||
template<typename Result> IModeration::LinkedAccount ReadLinked(Result& r, eAccountLink link) {
|
||||
return { r->getUInt("id"), std::string(r->getString("name").c_str()), static_cast<uint8_t>(r->getInt("gm_level")), r->getBoolean("banned"),
|
||||
static_cast<uint8_t>(link), r->getUInt("shared"), r->getInt64("last_seen") };
|
||||
}
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetStrikeStep(uint64_t strikeId, const std::string& step, uint32_t count) {
|
||||
ExecuteUpdate("UPDATE account_strikes SET step = ?, step_count = ? WHERE id = ?;", step, count, strikeId);
|
||||
}
|
||||
|
||||
std::vector<IModeration::AppliedStrikeStep> MySQLDatabase::GetAppliedStrikeSteps(uint32_t accountId, int64_t since) {
|
||||
std::vector<AppliedStrikeStep> steps;
|
||||
auto result = ExecuteSelect("SELECT step, step_count, created_at FROM account_strikes WHERE account_id = ? AND step != '' AND created_at >= ? ORDER BY id;", accountId, since);
|
||||
while (result->next()) steps.push_back({ std::string(result->getString("step").c_str()), result->getUInt("step_count"), result->getInt64("created_at") });
|
||||
return steps;
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::InsertPlayerReport(const PlayerReport& r) {
|
||||
ExecuteInsert("INSERT INTO player_reports (created_at, kind, reporter_id, reporter_account_id, object_id, object_lot, target_character_id, target_account_id, "
|
||||
"property_id, zone_id, instance_id, clone_id, body, resolution) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '');",
|
||||
r.createdAt, r.kind, r.reporterId, r.reporterAccountId, r.objectId, r.objectLot, r.targetCharacterId, r.targetAccountId, r.propertyId,
|
||||
r.zoneId, r.instanceId, r.cloneId, r.body);
|
||||
auto last = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;");
|
||||
return last->next() ? last->getUInt64("id") : 0;
|
||||
}
|
||||
|
||||
std::vector<IModeration::PlayerReport> MySQLDatabase::GetPlayerReports(const PlayerReportQuery& q) {
|
||||
std::vector<PlayerReport> reports;
|
||||
auto result = ExecuteSelect("SELECT * FROM player_reports" + REPORTS_WHERE + " ORDER BY id DESC LIMIT ? OFFSET ?;", q.status, q.status, q.accountId, q.accountId, q.limit, q.offset);
|
||||
while (result->next()) reports.push_back(ReadReport(result));
|
||||
return reports;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::CountPlayerReports(const PlayerReportQuery& q) {
|
||||
auto result = ExecuteSelect("SELECT COUNT(*) AS count FROM player_reports" + REPORTS_WHERE + ";", q.status, q.status, q.accountId, q.accountId);
|
||||
return result->next() ? result->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
std::optional<IModeration::PlayerReport> MySQLDatabase::GetPlayerReport(uint64_t id) {
|
||||
auto result = ExecuteSelect("SELECT * FROM player_reports WHERE id = ?;", id);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return ReadReport(result);
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetPlayerReportStatus(uint64_t id, uint8_t status, const std::string& handledBy, const std::string& resolution, int64_t time) {
|
||||
ExecuteUpdate("UPDATE player_reports SET status = ?, handled_by = ?, resolution = ?, handled_at = ? WHERE id = ?;", status, handledBy, resolution, time, id);
|
||||
}
|
||||
|
||||
std::vector<IModeration::ChatFilterWord> MySQLDatabase::GetChatFilterWords() {
|
||||
std::vector<ChatFilterWord> words;
|
||||
auto result = ExecuteSelect("SELECT * FROM chat_filter_words ORDER BY word;");
|
||||
while (result->next()) {
|
||||
words.push_back({ std::string(result->getString("word").c_str()), result->getInt("allowed") != 0, std::string(result->getString("added_by").c_str()),
|
||||
result->getInt64("added_at") });
|
||||
}
|
||||
return words;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetChatFilterWord(const ChatFilterWord& w) {
|
||||
ExecuteInsert("INSERT INTO chat_filter_words (word, allowed, added_by, added_at) VALUES (?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE allowed = VALUES(allowed), added_by = VALUES(added_by), added_at = VALUES(added_at);", w.word, w.allowed, w.addedBy, w.addedAt);
|
||||
}
|
||||
|
||||
bool MySQLDatabase::DeleteChatFilterWord(const std::string& word) {
|
||||
return ExecuteUpdate("DELETE FROM chat_filter_words WHERE word = ?;", word) > 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::RecordLoginAddress(uint32_t accountId, const std::string& address, int64_t time) {
|
||||
ExecuteInsert("INSERT INTO account_login_addresses (account_id, address, first_seen, last_seen) VALUES (?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE last_seen = VALUES(last_seen), logins = logins + 1;", accountId, address, time, time);
|
||||
}
|
||||
|
||||
std::vector<IModeration::LinkedAccount> MySQLDatabase::GetLinkedAccounts(uint32_t accountId) {
|
||||
std::vector<LinkedAccount> links;
|
||||
auto keys = ExecuteSelect("SELECT a.id, a.name, a.gm_level, a.banned, 0 AS last_seen, "
|
||||
"(SELECT COUNT(*) FROM accounts k WHERE k.play_key_id = me.play_key_id) AS shared "
|
||||
"FROM accounts me JOIN accounts a ON a.play_key_id = me.play_key_id AND a.id != me.id "
|
||||
"WHERE me.id = ? AND me.play_key_id IS NOT NULL AND me.play_key_id != 0 ORDER BY a.id LIMIT 200;", accountId);
|
||||
while (keys->next()) links.push_back(ReadLinked(keys, eAccountLink::PLAY_KEY));
|
||||
|
||||
auto emails = ExecuteSelect("SELECT a.id, a.name, a.gm_level, a.banned, 0 AS last_seen, 1 AS shared "
|
||||
"FROM accounts me JOIN accounts a ON LOWER(a.email) = LOWER(me.email) AND a.id != me.id "
|
||||
"WHERE me.id = ? AND me.email IS NOT NULL AND me.email != '' ORDER BY a.id LIMIT 200;", accountId);
|
||||
while (emails->next()) links.push_back(ReadLinked(emails, eAccountLink::EMAIL));
|
||||
|
||||
auto addresses = ExecuteSelect("SELECT a.id, a.name, a.gm_level, a.banned, COUNT(DISTINCT o.address) AS shared, MAX(o.last_seen) AS last_seen "
|
||||
"FROM account_login_addresses me JOIN account_login_addresses o ON o.address = me.address AND o.account_id != me.account_id "
|
||||
"JOIN accounts a ON a.id = o.account_id WHERE me.account_id = ? GROUP BY a.id, a.name, a.gm_level, a.banned ORDER BY last_seen DESC LIMIT 200;", accountId);
|
||||
while (addresses->next()) links.push_back(ReadLinked(addresses, eAccountLink::LOGIN_ADDRESS));
|
||||
return links;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::CountLoginAddresses(uint32_t accountId) {
|
||||
auto result = ExecuteSelect("SELECT COUNT(*) AS count FROM account_login_addresses WHERE account_id = ?;", accountId);
|
||||
return result->next() ? result->getUInt("count") : 0;
|
||||
}
|
||||
@@ -1,18 +1,23 @@
|
||||
#include "MySQLDatabase.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "json.hpp"
|
||||
|
||||
void MySQLDatabase::SetPetNameModerationStatus(const LWOOBJID& petId, const IPetNames::Info& info) {
|
||||
const auto owner = info.ownerId == 0 ? std::optional<LWOOBJID>{} : std::optional<LWOOBJID>{ info.ownerId };
|
||||
ExecuteInsert(
|
||||
"INSERT INTO `pet_names` (`id`, `pet_name`, `approved`) VALUES (?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE pet_name = ?, approved = ?;",
|
||||
"INSERT INTO `pet_names` (`id`, `pet_name`, `approved`, `owner_id`) VALUES (?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE pet_name = ?, approved = ?, owner_id = COALESCE(?, owner_id);",
|
||||
petId,
|
||||
info.petName,
|
||||
info.approvalStatus,
|
||||
owner,
|
||||
info.petName,
|
||||
info.approvalStatus);
|
||||
info.approvalStatus,
|
||||
owner);
|
||||
}
|
||||
|
||||
std::optional<IPetNames::Info> MySQLDatabase::GetPetNameInfo(const LWOOBJID& petId) {
|
||||
auto result = ExecuteSelect("SELECT pet_name, approved FROM pet_names WHERE id = ? LIMIT 1;", petId);
|
||||
auto result = ExecuteSelect("SELECT pet_name, approved, owner_id FROM pet_names WHERE id = ? LIMIT 1;", petId);
|
||||
|
||||
if (!result->next()) {
|
||||
return std::nullopt;
|
||||
@@ -21,6 +26,75 @@ std::optional<IPetNames::Info> MySQLDatabase::GetPetNameInfo(const LWOOBJID& pet
|
||||
IPetNames::Info toReturn;
|
||||
toReturn.petName = result->getString("pet_name").c_str();
|
||||
toReturn.approvalStatus = result->getInt("approved");
|
||||
toReturn.ownerId = result->isNull("owner_id") ? 0 : result->getInt64("owner_id");
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
std::string MySQLDatabase::GetPetNamesTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc, bool pendingOnly) {
|
||||
// A number in the search box also matches IDs exactly (-1 never matches); names match the pet or its owner
|
||||
const int64_t searchId = GeneralUtils::TryParse<int64_t>(std::string(search)).value_or(-1);
|
||||
const std::string from = " FROM pet_names p LEFT JOIN charinfo c ON c.id = p.owner_id";
|
||||
std::string whereClause;
|
||||
// approved: 1 = pending moderation, 2 = approved (see PetComponent)
|
||||
const std::string pendingFilter = pendingOnly ? "p.approved = 1" : "";
|
||||
const std::string searchFilter = "(p.pet_name LIKE CONCAT('%', ?, '%') OR p.id = ? OR c.name LIKE CONCAT('%', ?, '%'))";
|
||||
if (pendingOnly && !search.empty()) whereClause = " WHERE " + pendingFilter + " AND " + searchFilter;
|
||||
else if (pendingOnly) whereClause = " WHERE " + pendingFilter;
|
||||
else if (!search.empty()) whereClause = " WHERE " + searchFilter;
|
||||
|
||||
std::string orderColumnName = "p.id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "p.id"; break;
|
||||
case 1: orderColumnName = "p.pet_name"; break;
|
||||
case 2: orderColumnName = "p.approved"; break;
|
||||
case 3: orderColumnName = "c.name"; break;
|
||||
}
|
||||
std::string orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
std::string mainQuery = "SELECT p.id, p.pet_name, p.approved, p.owner_id, c.name AS owner_name" + from + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
auto totalCountResult = ExecuteSelect("SELECT COUNT(*) as count FROM pet_names p" + (pendingOnly ? " WHERE " + pendingFilter : std::string{}) + ";");
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
auto filteredCountResult = ExecuteSelect("SELECT COUNT(*) as count" + from + whereClause + ";", search, searchId, search);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
auto result = !search.empty()
|
||||
? ExecuteSelect(mainQuery, search, searchId, search, start, length)
|
||||
: ExecuteSelect(mainQuery, start, length);
|
||||
|
||||
nlohmann::json dataArray = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
const bool hasOwner = !result->isNull("owner_id") && result->getInt64("owner_id") != 0;
|
||||
dataArray.push_back({
|
||||
{"id", std::to_string(result->getInt64("id"))},
|
||||
{"pet_name", result->getString("pet_name")},
|
||||
{"approved", result->getInt("approved")},
|
||||
{"owner_id", hasOwner ? std::to_string(result->getInt64("owner_id")) : ""},
|
||||
{"owner_name", std::string(result->isNull("owner_name") ? "" : result->getString("owner_name").c_str())}
|
||||
});
|
||||
}
|
||||
return nlohmann::json({{"draw", 0}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", dataArray}}).dump();
|
||||
}
|
||||
|
||||
void MySQLDatabase::ApprovePetName(const int64_t id) {
|
||||
ExecuteUpdate("UPDATE pet_names SET approved = 2 WHERE id = ?;", id);
|
||||
}
|
||||
|
||||
void MySQLDatabase::RejectPetName(const int64_t id) {
|
||||
ExecuteDelete("DELETE FROM pet_names WHERE id = ?;", id);
|
||||
}
|
||||
|
||||
std::vector<LWOOBJID> MySQLDatabase::GetPetsWithUnknownOwner() {
|
||||
std::vector<LWOOBJID> pets;
|
||||
auto result = ExecuteSelect("SELECT id FROM pet_names WHERE owner_id IS NULL;");
|
||||
while (result->next()) pets.push_back(result->getInt64("id"));
|
||||
return pets;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetPetOwner(const LWOOBJID petId, const LWOOBJID ownerId) {
|
||||
ExecuteUpdate("UPDATE pet_names SET owner_id = ? WHERE id = ?;", ownerId, petId);
|
||||
}
|
||||
|
||||
@@ -9,3 +9,19 @@ std::optional<bool> MySQLDatabase::IsPlaykeyActive(const int32_t playkeyId) {
|
||||
|
||||
return keyCheckRes->getBoolean("active");
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
void MySQLDatabase::CreatePlayKey(const std::string_view keyString, const uint32_t uses, const std::string_view notes) {
|
||||
ExecuteInsert("INSERT INTO play_keys (key_string, key_uses, active, created_at, notes) VALUES (?, ?, 1, NOW(), ?);", keyString, uses, notes);
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetPlayKeyActive(const int32_t playkeyId, const bool active) {
|
||||
ExecuteUpdate("UPDATE play_keys SET active = ? WHERE id = ?;", active ? 1 : 0, playkeyId);
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::GetPlayKeyCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM play_keys;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
|
||||
70
dDatabase/GameDatabase/MySQL/Tables/PlayerPositions.cpp
Normal file
70
dDatabase/GameDatabase/MySQL/Tables/PlayerPositions.cpp
Normal file
@@ -0,0 +1,70 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
void MySQLDatabase::InsertPositionSamples(const std::vector<PositionSample>& samples) {
|
||||
if (samples.empty()) return;
|
||||
const auto prevCommit = GetAutoCommit();
|
||||
SetAutoCommit(false);
|
||||
for (const auto& sample : samples) {
|
||||
ExecuteInsert("REPLACE INTO player_positions (character_id, time, zone_id, instance_id, clone_id, x, y, z) VALUES (?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
sample.characterId, sample.time, sample.zoneId, sample.instanceId, sample.cloneId, sample.x, sample.y, sample.z);
|
||||
}
|
||||
Commit();
|
||||
SetAutoCommit(prevCommit);
|
||||
}
|
||||
|
||||
std::vector<IPlayerPositions::PositionSample> MySQLDatabase::GetPositionSamples(uint32_t zoneId, uint32_t instanceId, int64_t from, int64_t to, int64_t bucketSeconds, uint32_t limit) {
|
||||
const std::string instance = instanceId ? " AND instance_id = ?" : "";
|
||||
const std::string query = bucketSeconds > 1
|
||||
? "SELECT character_id, instance_id, MAX(clone_id) AS clone_id, MIN(time) AS t, AVG(x) AS x, AVG(y) AS y, AVG(z) AS z FROM player_positions "
|
||||
"WHERE zone_id = ? AND time BETWEEN ? AND ?" + instance + " GROUP BY character_id, instance_id, FLOOR(time / ?) ORDER BY character_id, t LIMIT ?;"
|
||||
: "SELECT character_id, instance_id, clone_id, time AS t, x, y, z FROM player_positions WHERE zone_id = ? AND time BETWEEN ? AND ?" + instance +
|
||||
" ORDER BY character_id, t LIMIT ?;";
|
||||
auto result = bucketSeconds > 1
|
||||
? (instanceId ? ExecuteSelect(query, zoneId, from, to, instanceId, bucketSeconds, limit) : ExecuteSelect(query, zoneId, from, to, bucketSeconds, limit))
|
||||
: (instanceId ? ExecuteSelect(query, zoneId, from, to, instanceId, limit) : ExecuteSelect(query, zoneId, from, to, limit));
|
||||
std::vector<PositionSample> samples;
|
||||
while (result->next()) {
|
||||
samples.push_back({ result->getInt64("t"), result->getInt64("character_id"), zoneId, result->getUInt("instance_id"), result->getUInt("clone_id"),
|
||||
static_cast<float>(result->getDouble("x")), static_cast<float>(result->getDouble("y")), static_cast<float>(result->getDouble("z")) });
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
std::vector<IPlayerPositions::PositionInstance> MySQLDatabase::GetPositionInstances(uint32_t zoneId, int64_t from, int64_t to) {
|
||||
const std::string query = "SELECT zone_id, instance_id, MAX(clone_id) AS clone_id, MIN(time) AS first, MAX(time) AS last, COUNT(DISTINCT character_id) AS players "
|
||||
"FROM player_positions WHERE time BETWEEN ? AND ?" + std::string(zoneId ? " AND zone_id = ?" : "") + " GROUP BY zone_id, instance_id ORDER BY last DESC LIMIT 500;";
|
||||
auto result = zoneId ? ExecuteSelect(query, from, to, zoneId) : ExecuteSelect(query, from, to);
|
||||
std::vector<PositionInstance> instances;
|
||||
while (result->next()) {
|
||||
instances.push_back({ result->getUInt("zone_id"), result->getUInt("instance_id"), result->getUInt("clone_id"),
|
||||
result->getInt64("first"), result->getInt64("last"), result->getUInt("players") });
|
||||
}
|
||||
return instances;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::PrunePositionSamples(int64_t beforeTime) {
|
||||
return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM player_positions WHERE time < ?;", beforeTime));
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetMapCellsPerDay(uint32_t zoneId, std::optional<uint32_t> cloneId, uint8_t kind, uint32_t fromDay, uint32_t toDay, uint32_t limit) {
|
||||
const std::string clone = cloneId ? " AND clone_id = " + std::to_string(*cloneId) : "";
|
||||
auto result = ExecuteSelect("SELECT day, cell_x, cell_z, SUM(events) AS events FROM map_events_daily WHERE zone = ? AND kind = ? AND day BETWEEN ? AND ?" + clone +
|
||||
" GROUP BY day, cell_x, cell_z ORDER BY day, cell_x, cell_z LIMIT ?;", zoneId, static_cast<uint32_t>(kind), fromDay, toDay, limit);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"day", result->getInt("day")}, {"x", result->getInt("cell_x")}, {"z", result->getInt("cell_z")}, {"events", result->getInt64("events")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetCloneVisitors(uint32_t zoneId, uint32_t cloneId, int64_t from, int64_t to, uint32_t limit) {
|
||||
auto result = ExecuteSelect("SELECT p.character_id, COALESCE(c.name, '') AS name, MIN(p.time) AS first, MAX(p.time) AS last FROM player_positions p "
|
||||
"LEFT JOIN charinfo c ON c.id = p.character_id WHERE p.zone_id = ? AND p.clone_id = ? AND p.time BETWEEN ? AND ? "
|
||||
"GROUP BY p.character_id, c.name ORDER BY last DESC, p.character_id LIMIT ?;", zoneId, cloneId, from, to, limit);
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"character_id", std::to_string(result->getInt64("character_id"))}, {"name", std::string(result->getString("name").c_str())},
|
||||
{"first", result->getInt64("first")}, {"last", result->getInt64("last")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "MySQLDatabase.h"
|
||||
#include "GeneralUtils.h"
|
||||
#include "ePropertySortType.h"
|
||||
|
||||
IProperty::Info ReadPropertyInfo(PreparedStmtResultSet& result) {
|
||||
@@ -15,6 +16,7 @@ IProperty::Info ReadPropertyInfo(PreparedStmtResultSet& result) {
|
||||
info.reputation = result->getUInt("reputation");
|
||||
info.modApproved = result->getUInt("mod_approved");
|
||||
info.performanceCost = result->getFloat("performance_cost");
|
||||
info.zoneId = result->getUInt("zone_id");
|
||||
return info;
|
||||
}
|
||||
|
||||
@@ -139,7 +141,7 @@ IProperty::PropertyEntranceResult MySQLDatabase::GetProperties(const IProperty::
|
||||
|
||||
std::optional<IProperty::Info> MySQLDatabase::GetPropertyInfo(const LWOMAPID mapId, const LWOCLONEID cloneId) {
|
||||
auto propertyEntry = ExecuteSelect(
|
||||
"SELECT id, owner_id, clone_id, name, description, privacy_option, rejection_reason, last_updated, time_claimed, reputation, mod_approved, performance_cost "
|
||||
"SELECT id, owner_id, clone_id, name, description, privacy_option, rejection_reason, last_updated, time_claimed, reputation, mod_approved, performance_cost, zone_id "
|
||||
"FROM properties WHERE zone_id = ? AND clone_id = ?;", mapId, cloneId);
|
||||
|
||||
if (!propertyEntry->next()) {
|
||||
@@ -186,7 +188,7 @@ void MySQLDatabase::InsertNewProperty(const IProperty::Info& info, const uint32_
|
||||
|
||||
std::optional<IProperty::Info> MySQLDatabase::GetPropertyInfo(const LWOOBJID id) {
|
||||
auto propertyEntry = ExecuteSelect(
|
||||
"SELECT id, owner_id, clone_id, name, description, privacy_option, rejection_reason, last_updated, time_claimed, reputation, mod_approved, performance_cost "
|
||||
"SELECT id, owner_id, clone_id, name, description, privacy_option, rejection_reason, last_updated, time_claimed, reputation, mod_approved, performance_cost, zone_id "
|
||||
"FROM properties WHERE id = ?;", id);
|
||||
|
||||
if (!propertyEntry->next()) {
|
||||
@@ -195,3 +197,121 @@ std::optional<IProperty::Info> MySQLDatabase::GetPropertyInfo(const LWOOBJID id)
|
||||
|
||||
return ReadPropertyInfo(propertyEntry);
|
||||
}
|
||||
|
||||
#include "json.hpp"
|
||||
|
||||
uint32_t MySQLDatabase::GetPropertyCount() {
|
||||
auto res = ExecuteSelect("SELECT COUNT(*) as count FROM properties;");
|
||||
return res->next() ? res->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
std::string MySQLDatabase::GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search, uint32_t orderColumn, bool orderAsc, bool pendingOnly) {
|
||||
// A number in the search box also matches IDs exactly (-1 never matches)
|
||||
const int64_t searchId = GeneralUtils::TryParse<int64_t>(std::string(search)).value_or(-1);
|
||||
// Build base query
|
||||
std::string baseQuery = "SELECT id, owner_id, name, mod_approved, reputation, performance_cost, zone_id, (SELECT c.name FROM charinfo c WHERE c.id = properties.owner_id) AS owner_name, (SELECT COUNT(*) FROM properties_contents pc WHERE pc.property_id = properties.id) AS models FROM properties";
|
||||
std::string whereClause;
|
||||
std::string orderClause;
|
||||
|
||||
// Only public properties awaiting review show up in the moderation queue
|
||||
const std::string pendingFilter = pendingOnly ? "mod_approved = 0 AND privacy_option = 2 AND rejection_reason = ''" : "";
|
||||
const std::string searchFilter = !search.empty() ? "(name LIKE CONCAT('%', ?, '%') OR owner_id IN (SELECT id FROM charinfo WHERE name LIKE CONCAT('%', ?, '%')) OR id = ? OR owner_id = ?)" : "";
|
||||
if (!pendingFilter.empty() && !searchFilter.empty()) whereClause = " WHERE " + pendingFilter + " AND " + searchFilter;
|
||||
else if (!pendingFilter.empty()) whereClause = " WHERE " + pendingFilter;
|
||||
else if (!searchFilter.empty()) whereClause = " WHERE " + searchFilter;
|
||||
|
||||
// Map column indices to database columns
|
||||
std::string orderColumnName = "id";
|
||||
switch (orderColumn) {
|
||||
case 0: orderColumnName = "id"; break;
|
||||
case 1: orderColumnName = "name"; break;
|
||||
case 2: orderColumnName = "owner_id"; break;
|
||||
case 3: orderColumnName = "mod_approved"; break;
|
||||
case 4: orderColumnName = "reputation"; break;
|
||||
case 5: orderColumnName = "performance_cost"; break;
|
||||
case 6: orderColumnName = "zone_id"; break;
|
||||
default: orderColumnName = "id";
|
||||
}
|
||||
|
||||
orderClause = " ORDER BY " + orderColumnName + (orderAsc ? " ASC" : " DESC");
|
||||
|
||||
// Build the main query
|
||||
std::string mainQuery = baseQuery + whereClause + orderClause + " LIMIT ?, ?;";
|
||||
|
||||
// Get total count
|
||||
std::string totalCountQuery = "SELECT COUNT(*) as count FROM properties" + (pendingOnly ? " WHERE " + pendingFilter : std::string{}) + ";";
|
||||
auto totalCountResult = ExecuteSelect(totalCountQuery);
|
||||
uint32_t totalRecords = totalCountResult->next() ? totalCountResult->getUInt("count") : 0;
|
||||
|
||||
// Get filtered count
|
||||
uint32_t filteredRecords = totalRecords;
|
||||
if (!search.empty()) {
|
||||
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM properties" + whereClause + ";";
|
||||
auto filteredCountResult = ExecuteSelect(filteredCountQuery, search, search, searchId, searchId);
|
||||
filteredRecords = filteredCountResult->next() ? filteredCountResult->getUInt("count") : 0;
|
||||
}
|
||||
|
||||
// Execute main query
|
||||
auto result = !search.empty()
|
||||
? ExecuteSelect(mainQuery, search, search, searchId, searchId, start, length)
|
||||
: ExecuteSelect(mainQuery, start, length);
|
||||
|
||||
// Build response JSON
|
||||
nlohmann::json propertiesArray = nlohmann::json::array();
|
||||
|
||||
while (result->next()) {
|
||||
nlohmann::json property = {
|
||||
{"id", std::to_string(result->getUInt64("id"))},
|
||||
{"owner_id", std::to_string(result->getUInt64("owner_id"))},
|
||||
{"owner_name", result->isNull("owner_name") ? "" : std::string(result->getString("owner_name").c_str())},
|
||||
{"name", result->getString("name")},
|
||||
{"mod_approved", result->getBoolean("mod_approved")},
|
||||
{"reputation", result->getUInt64("reputation")},
|
||||
{"performance_cost", result->isNull("performance_cost") ? 0.0 : static_cast<double>(result->getDouble("performance_cost"))},
|
||||
{"models", result->getUInt("models")},
|
||||
{"zone_id", result->getUInt("zone_id")}
|
||||
};
|
||||
propertiesArray.push_back(property);
|
||||
}
|
||||
|
||||
nlohmann::json response = {
|
||||
{"draw", 0},
|
||||
{"recordsTotal", totalRecords},
|
||||
{"recordsFiltered", filteredRecords},
|
||||
{"data", propertiesArray}
|
||||
};
|
||||
|
||||
return response.dump();
|
||||
}
|
||||
|
||||
void MySQLDatabase::ApproveProperty(const LWOOBJID propertyId) {
|
||||
ExecuteUpdate("UPDATE properties SET mod_approved = 1 WHERE id = ?;", propertyId);
|
||||
}
|
||||
|
||||
IProperty::ShowcaseResult MySQLDatabase::GetShowcaseProperties(const IProperty::ShowcaseQuery& query) {
|
||||
// Public (privacy_option 2) and approved only; the owner must still exist, since the showcase names them
|
||||
const std::string from =
|
||||
" FROM properties p JOIN charinfo c ON c.id = p.owner_id"
|
||||
" WHERE p.mod_approved = 1 AND p.privacy_option = 2"
|
||||
" AND (? = 0 OR p.zone_id = ?)"
|
||||
" AND (? = '' OR p.name LIKE CONCAT('%', ?, '%') ESCAPE '!' OR p.description LIKE CONCAT('%', ?, '%') ESCAPE '!' OR c.name LIKE CONCAT('%', ?, '%') ESCAPE '!')";
|
||||
std::string order;
|
||||
switch (query.sort) {
|
||||
case ShowcaseSort::NEWEST: order = " ORDER BY p.last_updated DESC, p.id DESC"; break;
|
||||
case ShowcaseSort::NAME: order = " ORDER BY p.name ASC, p.id ASC"; break;
|
||||
default: order = " ORDER BY p.reputation DESC, p.last_updated DESC, p.id DESC"; break;
|
||||
}
|
||||
|
||||
IProperty::ShowcaseResult showcase;
|
||||
const auto& s = query.search;
|
||||
const auto like = GeneralUtils::LikeEscape(query.search, '!'); // a % or _ typed in the search box is matched literally
|
||||
auto count = ExecuteSelect("SELECT COUNT(*) AS count" + from + ";", query.zoneId, query.zoneId, s, like, like, like);
|
||||
showcase.total = count->next() ? count->getUInt("count") : 0;
|
||||
|
||||
auto result = ExecuteSelect("SELECT p.*, c.name AS owner_name, (SELECT COUNT(*) FROM properties_contents pc WHERE pc.property_id = p.id) AS models" +
|
||||
from + order + " LIMIT ?, ?;", query.zoneId, query.zoneId, s, like, like, like, query.start, query.length);
|
||||
while (result->next()) {
|
||||
showcase.entries.push_back({ ReadPropertyInfo(result), result->getString("owner_name").c_str(), result->getUInt("models") });
|
||||
}
|
||||
return showcase;
|
||||
}
|
||||
|
||||
106
dDatabase/GameDatabase/MySQL/Tables/RelatedData.cpp
Normal file
106
dDatabase/GameDatabase/MySQL/Tables/RelatedData.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
nlohmann::json MySQLDatabase::GetPropertiesOwnedBy(LWOOBJID characterId) {
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
auto result = ExecuteSelect("SELECT p.id, p.name, p.zone_id, p.privacy_option, p.mod_approved, p.rejection_reason, p.last_updated, p.reputation, (SELECT COUNT(*) FROM properties_contents c WHERE c.property_id = p.id) AS models FROM properties p WHERE p.owner_id = ? ORDER BY p.last_updated DESC;", characterId);
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"id", std::to_string(result->getInt64("id"))}, {"name", std::string(result->isNull("name") ? "" : result->getString("name").c_str())}, {"zone_id", result->getInt("zone_id")}, {"privacy_option", result->getInt("privacy_option")},
|
||||
{"mod_approved", result->getInt("mod_approved") != 0}, {"rejection_reason", std::string(result->isNull("rejection_reason") ? "" : result->getString("rejection_reason").c_str())}, {"last_updated", result->getInt64("last_updated")}, {"reputation", result->getInt64("reputation")}, {"models", result->getInt("models")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetBugReportsBy(LWOOBJID characterId, uint32_t limit) {
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
auto result = ExecuteSelect("SELECT id, body, submitted, resolved_time FROM bug_reports WHERE reporter_id = ? ORDER BY id DESC LIMIT ?;", characterId, limit);
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"id", result->getInt("id")}, {"body", std::string(result->isNull("body") ? "" : result->getString("body").c_str()).substr(0, 200)}, {"submitted", std::string(result->isNull("submitted") ? "" : result->getString("submitted").c_str())}, {"resolved", !result->isNull("resolved_time")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetEconomyFlagsFor(LWOOBJID characterId, uint32_t limit) {
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
auto result = ExecuteSelect("SELECT id, created_at, kind, details, status FROM economy_flags WHERE character_id = ? ORDER BY id DESC LIMIT ?;", characterId, limit);
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"id", result->getInt64("id")}, {"created_at", result->getInt64("created_at")}, {"kind", result->getInt("kind")}, {"details", std::string(result->isNull("details") ? "" : result->getString("details").c_str())}, {"status", result->getInt("status")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetFriendsOf(LWOOBJID characterId) {
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
auto result = ExecuteSelect("SELECT f.friend_id AS id, f.best_friend AS best, c.name FROM friends f LEFT JOIN charinfo c ON c.id = f.friend_id WHERE f.player_id = ? UNION SELECT f.player_id AS id, f.best_friend AS best, c.name FROM friends f LEFT JOIN charinfo c ON c.id = f.player_id WHERE f.friend_id = ?;", characterId, characterId);
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"id", std::to_string(result->getInt64("id"))}, {"name", std::string(result->isNull("name") ? "" : result->getString("name").c_str())}, {"best", result->getInt("best") != 0} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetCheatDetectionsFor(uint32_t accountId, uint32_t limit) {
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
auto result = ExecuteSelect("SELECT id, name, violation_msg, violation_time FROM player_cheat_detections WHERE account_id = ? ORDER BY id DESC LIMIT ?;", accountId, limit);
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"id", result->getInt64("id")}, {"name", std::string(result->isNull("name") ? "" : result->getString("name").c_str())}, {"message", std::string(result->isNull("violation_msg") ? "" : result->getString("violation_msg").c_str())}, {"time", std::string(result->isNull("violation_time") ? "" : result->getString("violation_time").c_str())} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetAuditAbout(uint32_t accountId, uint32_t limit) {
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
auto result = ExecuteSelect("SELECT id, account_name, action, description, timestamp FROM audit_log WHERE target_account_id = ? ORDER BY id DESC LIMIT ?;", accountId, limit);
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"id", result->getInt64("id")}, {"actor", std::string(result->isNull("account_name") ? "" : result->getString("account_name").c_str())}, {"action", std::string(result->isNull("action") ? "" : result->getString("action").c_str())}, {"description", std::string(result->isNull("description") ? "" : result->getString("description").c_str())}, {"time", result->getInt64("timestamp")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertModerationDecision(const std::string& kind, int64_t subjectId, const std::string& subject, bool approved, const std::string& reason, int64_t time) {
|
||||
ExecuteInsert("INSERT INTO moderation_decisions (kind, subject_id, subject, approved, reason, decided_at) VALUES (?, ?, ?, ?, ?, ?);", kind, subjectId, subject, approved, reason, time);
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetModerationDecisions(const std::string& kind, int64_t subjectId, uint32_t limit) {
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
auto result = ExecuteSelect("SELECT subject, approved, reason, decided_at FROM moderation_decisions WHERE kind = ? AND subject_id = ? ORDER BY id DESC LIMIT ?;", kind, subjectId, limit);
|
||||
while (result->next()) {
|
||||
rows.push_back({ {"subject", result->getString("subject").c_str()}, {"approved", result->getInt("approved") != 0}, {"reason", result->getString("reason").c_str()}, {"time", result->getInt64("decided_at")} });
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetPropertyRecord(LWOOBJID propertyId) {
|
||||
auto result = ExecuteSelect("SELECT id, owner_id, template_id, clone_id, name, description, rent_amount, rent_due, privacy_option, mod_approved, last_updated, time_claimed, rejection_reason, reputation, zone_id, performance_cost FROM properties WHERE id = ? LIMIT 1;", propertyId);
|
||||
if (!result->next()) return nlohmann::json::object();
|
||||
const auto text = [&result](const char* col) { return std::string(result->isNull(col) ? "" : result->getString(col).c_str()); };
|
||||
const auto id = [&result](const char* col) -> nlohmann::json { return result->isNull(col) ? nlohmann::json() : nlohmann::json(std::to_string(result->getInt64(col))); };
|
||||
return {
|
||||
{"id", id("id")}, {"owner_id", id("owner_id")}, {"template_id", result->getInt("template_id")}, {"clone_id", id("clone_id")},
|
||||
{"name", text("name")}, {"description", text("description")}, {"rent_amount", result->getInt("rent_amount")}, {"rent_due", result->getInt64("rent_due")},
|
||||
{"privacy_option", result->getInt("privacy_option")}, {"mod_approved", result->getInt("mod_approved")}, {"last_updated", result->getInt64("last_updated")},
|
||||
{"time_claimed", result->getInt64("time_claimed")}, {"rejection_reason", text("rejection_reason")}, {"reputation", result->getInt64("reputation")},
|
||||
{"zone_id", result->getInt("zone_id")}, {"performance_cost", result->isNull("performance_cost") ? 0.0 : static_cast<double>(result->getDouble("performance_cost"))}
|
||||
};
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetPropertyModelRecords(LWOOBJID propertyId) {
|
||||
nlohmann::json rows = nlohmann::json::array();
|
||||
auto result = ExecuteSelect("SELECT pc.id, pc.property_id, pc.ugc_id, pc.lot, pc.x, pc.y, pc.z, pc.rx, pc.ry, pc.rz, pc.rw, pc.model_name, pc.model_description, "
|
||||
"pc.behavior_1, pc.behavior_2, pc.behavior_3, pc.behavior_4, pc.behavior_5, u.account_id, u.character_id AS ugc_character_id, u.is_optimized, u.bake_ao, u.filename, "
|
||||
"LENGTH(u.lxfml) AS lxfml_size, mb.ldf_config, c.name AS creator_name FROM properties_contents pc LEFT JOIN ugc u ON u.id = pc.ugc_id "
|
||||
"LEFT JOIN ugc_modular_build mb ON mb.ugc_id = pc.ugc_id LEFT JOIN charinfo c ON c.id = u.character_id WHERE pc.property_id = ? ORDER BY pc.id;", propertyId);
|
||||
while (result->next()) {
|
||||
const auto text = [&result](const char* col) -> nlohmann::json { return result->isNull(col) ? nlohmann::json() : nlohmann::json(std::string(result->getString(col).c_str())); };
|
||||
const auto id = [&result](const char* col) -> nlohmann::json { return result->isNull(col) ? nlohmann::json() : nlohmann::json(std::to_string(result->getInt64(col))); };
|
||||
const auto num = [&result](const char* col) -> nlohmann::json { return result->isNull(col) ? nlohmann::json() : nlohmann::json(static_cast<int64_t>(result->getInt64(col))); };
|
||||
const auto dbl = [&result](const char* col) { return static_cast<double>(result->getDouble(col)); };
|
||||
rows.push_back({
|
||||
{"id", id("id")}, {"property_id", id("property_id")}, {"ugc_id", id("ugc_id")}, {"lot", result->getInt("lot")},
|
||||
{"x", dbl("x")}, {"y", dbl("y")}, {"z", dbl("z")}, {"rx", dbl("rx")}, {"ry", dbl("ry")}, {"rz", dbl("rz")}, {"rw", dbl("rw")},
|
||||
{"model_name", text("model_name")}, {"model_description", text("model_description")},
|
||||
{"behavior_1", id("behavior_1")}, {"behavior_2", id("behavior_2")}, {"behavior_3", id("behavior_3")}, {"behavior_4", id("behavior_4")}, {"behavior_5", id("behavior_5")},
|
||||
{"account_id", num("account_id")}, {"ugc_character_id", id("ugc_character_id")}, {"is_optimized", num("is_optimized")}, {"bake_ao", num("bake_ao")},
|
||||
{"filename", text("filename")}, {"lxfml_size", num("lxfml_size")}, {"ldf_config", text("ldf_config")}, {"creator_name", text("creator_name")}
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
94
dDatabase/GameDatabase/MySQL/Tables/ScheduledTasks.cpp
Normal file
94
dDatabase/GameDatabase/MySQL/Tables/ScheduledTasks.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
namespace {
|
||||
std::string Text(PreparedStmtResultSet& result, const char* field) {
|
||||
return result->isNull(field) ? "" : std::string(result->getString(field).c_str());
|
||||
}
|
||||
|
||||
IScheduledTasks::TaskRun RunRow(PreparedStmtResultSet& result, bool withLog) {
|
||||
IScheduledTasks::TaskRun run;
|
||||
run.id = result->getUInt64("id");
|
||||
run.task = Text(result, "task");
|
||||
run.trigger = Text(result, "task_trigger");
|
||||
run.actor = Text(result, "actor");
|
||||
run.startedAt = result->getInt64("started_at");
|
||||
run.finishedAt = result->getInt64("finished_at");
|
||||
run.status = static_cast<IScheduledTasks::eRunStatus>(result->getInt("status"));
|
||||
run.summary = Text(result, "summary");
|
||||
if (withLog) run.log = Text(result, "log");
|
||||
return run;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<IScheduledTasks::TaskSettings> MySQLDatabase::GetScheduledTasks() {
|
||||
std::vector<TaskSettings> tasks;
|
||||
auto result = ExecuteSelect("SELECT * FROM scheduled_tasks ORDER BY name;");
|
||||
while (result->next()) {
|
||||
TaskSettings task;
|
||||
task.name = Text(result, "name");
|
||||
if (!result->isNull("schedule")) task.schedule = Text(result, "schedule");
|
||||
task.enabled = result->getInt("enabled") != 0;
|
||||
task.lastScheduledAt = result->getInt64("last_scheduled_at");
|
||||
task.updatedAt = result->getInt64("updated_at");
|
||||
task.updatedBy = Text(result, "updated_by");
|
||||
tasks.push_back(std::move(task));
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetScheduledTask(const std::string& name, const std::optional<std::string>& schedule, bool enabled, const std::string& by) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO scheduled_tasks (name, schedule, enabled, updated_at, updated_by) VALUES (?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE schedule = VALUES(schedule), enabled = VALUES(enabled), updated_at = VALUES(updated_at), updated_by = VALUES(updated_by);",
|
||||
name, schedule, enabled, static_cast<int64_t>(std::time(nullptr)), by);
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetTaskLastScheduled(const std::string& name, int64_t time) {
|
||||
ExecuteInsert("INSERT INTO scheduled_tasks (name, last_scheduled_at) VALUES (?, ?) ON DUPLICATE KEY UPDATE last_scheduled_at = VALUES(last_scheduled_at);",
|
||||
name, time);
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertTaskRun(const TaskRun& run) {
|
||||
ExecuteInsert("INSERT INTO scheduled_task_runs (task, task_trigger, actor, started_at, finished_at, status, summary, log) VALUES (?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
run.task, run.trigger, run.actor, run.startedAt, run.finishedAt, static_cast<uint32_t>(run.status), run.summary, run.log);
|
||||
}
|
||||
|
||||
nlohmann::json MySQLDatabase::GetTaskRunsTable(const std::string& task, uint32_t start, uint32_t length) {
|
||||
const std::string where = task.empty() ? "" : " WHERE task = ?";
|
||||
uint32_t count = 0;
|
||||
{
|
||||
auto result = task.empty() ? ExecuteSelect("SELECT COUNT(*) AS count FROM scheduled_task_runs;")
|
||||
: ExecuteSelect("SELECT COUNT(*) AS count FROM scheduled_task_runs" + where + ";", task);
|
||||
if (result->next()) count = result->getUInt("count");
|
||||
}
|
||||
const std::string query = "SELECT id, task, task_trigger, actor, started_at, finished_at, status, summary FROM scheduled_task_runs" + where + " ORDER BY id DESC LIMIT ? OFFSET ?;";
|
||||
auto result = task.empty() ? ExecuteSelect(query, length, start) : ExecuteSelect(query, task, length, start);
|
||||
nlohmann::json data = nlohmann::json::array();
|
||||
while (result->next()) {
|
||||
const auto run = RunRow(result, false);
|
||||
data.push_back({ {"id", run.id}, {"task", run.task}, {"trigger", run.trigger}, {"actor", run.actor}, {"started_at", run.startedAt},
|
||||
{"finished_at", run.finishedAt}, {"status", static_cast<uint32_t>(run.status)}, {"summary", run.summary} });
|
||||
}
|
||||
return { {"draw", 0}, {"recordsTotal", count}, {"recordsFiltered", count}, {"data", data} };
|
||||
}
|
||||
|
||||
std::optional<IScheduledTasks::TaskRun> MySQLDatabase::GetTaskRun(uint64_t id) {
|
||||
auto result = ExecuteSelect("SELECT * FROM scheduled_task_runs WHERE id = ?;", static_cast<int64_t>(id));
|
||||
if (!result->next()) return std::nullopt;
|
||||
return RunRow(result, true);
|
||||
}
|
||||
|
||||
std::vector<IScheduledTasks::TaskRun> MySQLDatabase::GetLatestTaskRuns() {
|
||||
std::vector<TaskRun> runs;
|
||||
auto result = ExecuteSelect(
|
||||
"SELECT r.id, r.task, r.task_trigger, r.actor, r.started_at, r.finished_at, r.status, r.summary FROM scheduled_task_runs r "
|
||||
"JOIN (SELECT task, MAX(id) AS id FROM scheduled_task_runs GROUP BY task) latest ON latest.id = r.id ORDER BY r.task;");
|
||||
while (result->next()) runs.push_back(RunRow(result, false));
|
||||
return runs;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::PruneTaskRuns(int64_t beforeTime) {
|
||||
return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM scheduled_task_runs WHERE finished_at < ?;", beforeTime));
|
||||
}
|
||||
97
dDatabase/GameDatabase/MySQL/Tables/ServerConfig.cpp
Normal file
97
dDatabase/GameDatabase/MySQL/Tables/ServerConfig.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
void MySQLDatabase::ReportConfigFromFile(const Setting& setting) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO server_config (file, name, file_value, file_source, secret, description, seen_at) VALUES (?, ?, ?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE file_value = VALUES(file_value), file_source = VALUES(file_source), secret = VALUES(secret), "
|
||||
"description = IF(VALUES(description) = '', description, VALUES(description)), seen_at = VALUES(seen_at);",
|
||||
setting.file, setting.name, setting.fileValue ? std::optional<std::string>(*setting.fileValue) : std::nullopt, setting.fileSource,
|
||||
setting.secret, setting.description, static_cast<int64_t>(std::time(nullptr)));
|
||||
}
|
||||
|
||||
std::vector<IServerConfig::Setting> MySQLDatabase::GetServerConfig(const std::vector<std::string>& files) {
|
||||
std::vector<Setting> settings;
|
||||
auto result = ExecuteSelect("SELECT * FROM server_config ORDER BY file, name;");
|
||||
const auto text = [&](const char* field) { return std::string(result->getString(field).c_str()); };
|
||||
while (result->next()) {
|
||||
Setting setting;
|
||||
setting.file = text("file");
|
||||
if (!files.empty() && std::find(files.begin(), files.end(), setting.file) == files.end()) continue;
|
||||
setting.name = text("name");
|
||||
if (!result->isNull("file_value")) setting.fileValue = text("file_value");
|
||||
setting.fileSource = text("file_source");
|
||||
if (!result->isNull("web_value")) setting.webValue = text("web_value");
|
||||
setting.webWins = result->getInt("web_wins") != 0;
|
||||
setting.secret = result->getInt("secret") != 0;
|
||||
setting.description = result->isNull("description") ? "" : text("description");
|
||||
setting.seenAt = result->getInt64("seen_at");
|
||||
setting.updatedAt = result->getInt64("updated_at");
|
||||
setting.updatedBy = text("updated_by");
|
||||
settings.push_back(std::move(setting));
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetWebConfigValue(const std::string& file, const std::string& name, const std::optional<std::string>& value, bool webWins, const std::string& by) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO server_config (file, name, web_value, web_wins, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE web_value = VALUES(web_value), web_wins = VALUES(web_wins), updated_at = VALUES(updated_at), updated_by = VALUES(updated_by);",
|
||||
file, name, value, webWins, static_cast<int64_t>(std::time(nullptr)), by);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteServerConfig(const std::string& file, const std::string& name) {
|
||||
ExecuteDelete("DELETE FROM server_config WHERE file = ? AND name = ?;", file, name);
|
||||
}
|
||||
|
||||
namespace {
|
||||
IServerConfig::SettingChange ReadSettingChange(PreparedStmtResultSet& result) {
|
||||
const auto text = [&](const char* field) { return std::string(result->getString(field).c_str()); };
|
||||
const auto optional = [&](const char* field) { return result->isNull(field) ? std::nullopt : std::optional<std::string>(text(field)); };
|
||||
IServerConfig::SettingChange change;
|
||||
change.id = result->getUInt64("id");
|
||||
change.file = text("file");
|
||||
change.name = text("name");
|
||||
change.oldValue = optional("old_value");
|
||||
change.oldWebWins = result->getInt("old_web_wins") != 0;
|
||||
change.newValue = optional("new_value");
|
||||
change.newWebWins = result->getInt("new_web_wins") != 0;
|
||||
change.fileValue = optional("file_value");
|
||||
change.secret = result->getInt("secret") != 0;
|
||||
change.removed = result->getInt("removed") != 0;
|
||||
change.revertOf = result->getUInt64("revert_of");
|
||||
change.changedAt = result->getInt64("changed_at");
|
||||
change.accountId = result->getUInt("account_id");
|
||||
change.changedBy = text("changed_by");
|
||||
return change;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::InsertSettingChange(const SettingChange& change) {
|
||||
ExecuteInsert(
|
||||
"INSERT INTO server_config_history (file, name, old_value, old_web_wins, new_value, new_web_wins, file_value, secret, removed, revert_of, changed_at, account_id, changed_by) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
change.file, change.name, change.oldValue, change.oldWebWins, change.newValue, change.newWebWins, change.fileValue, change.secret, change.removed,
|
||||
change.revertOf, change.changedAt, change.accountId, change.changedBy);
|
||||
auto result = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;"); // this connection's insert
|
||||
return result->next() ? result->getUInt64("id") : 0;
|
||||
}
|
||||
|
||||
std::vector<IServerConfig::SettingChange> MySQLDatabase::GetSettingChanges(const std::string& file, const std::string& name, uint32_t start, uint32_t length, uint64_t& total) {
|
||||
std::vector<SettingChange> changes;
|
||||
const bool all = file.empty() && name.empty();
|
||||
auto count = all ? ExecuteSelect("SELECT COUNT(*) AS count FROM server_config_history;")
|
||||
: ExecuteSelect("SELECT COUNT(*) AS count FROM server_config_history WHERE file = ? AND name = ?;", file, name);
|
||||
total = count->next() ? count->getUInt64("count") : 0;
|
||||
auto result = all ? ExecuteSelect("SELECT * FROM server_config_history ORDER BY id DESC LIMIT ? OFFSET ?;", length, start)
|
||||
: ExecuteSelect("SELECT * FROM server_config_history WHERE file = ? AND name = ? ORDER BY id DESC LIMIT ? OFFSET ?;", file, name, length, start);
|
||||
while (result->next()) changes.push_back(ReadSettingChange(result));
|
||||
return changes;
|
||||
}
|
||||
|
||||
std::optional<IServerConfig::SettingChange> MySQLDatabase::GetSettingChange(uint64_t id) {
|
||||
auto result = ExecuteSelect("SELECT * FROM server_config_history WHERE id = ?;", id);
|
||||
if (!result->next()) return std::nullopt;
|
||||
return ReadSettingChange(result);
|
||||
}
|
||||
40
dDatabase/GameDatabase/MySQL/Tables/ServerHealth.cpp
Normal file
40
dDatabase/GameDatabase/MySQL/Tables/ServerHealth.cpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
void MySQLDatabase::InsertHealthSample(const HealthSample& sample) {
|
||||
ExecuteInsert("REPLACE INTO server_health (time, players, worlds, auth_online, chat_online, memory_kb) VALUES (?, ?, ?, ?, ?, ?);",
|
||||
sample.time, sample.players, sample.worlds, sample.authOnline, sample.chatOnline, static_cast<int64_t>(sample.memoryKb));
|
||||
}
|
||||
|
||||
std::vector<IServerHealth::HealthSample> MySQLDatabase::GetHealthSamples(int64_t from, int64_t to, int64_t bucketSeconds) {
|
||||
std::vector<HealthSample> samples;
|
||||
auto result = ExecuteSelect("SELECT FLOOR(time / ?) * ? AS bucket, MAX(players) AS players, MAX(worlds) AS worlds, MIN(auth_online) AS auth, MIN(chat_online) AS chat, AVG(memory_kb) AS memory "
|
||||
"FROM server_health WHERE time >= ? AND time <= ? GROUP BY bucket ORDER BY bucket;", bucketSeconds, bucketSeconds, from, to);
|
||||
while (result->next()) {
|
||||
samples.push_back({ result->getInt64("bucket"), result->getUInt("players"), result->getUInt("worlds"),
|
||||
result->getInt("auth") != 0, result->getInt("chat") != 0, static_cast<uint64_t>(result->getDouble("memory")) });
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::PruneHealthSamples(int64_t beforeTime) {
|
||||
ExecuteDelete("DELETE FROM server_health_instances WHERE time < ?;", beforeTime);
|
||||
return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM server_health WHERE time < ?;", beforeTime));
|
||||
}
|
||||
|
||||
void MySQLDatabase::InsertInstanceSamples(const std::vector<InstanceSample>& samples) {
|
||||
for (const auto& sample : samples) {
|
||||
ExecuteInsert("REPLACE INTO server_health_instances (time, zone_id, instance_id, clone_id, players) VALUES (?, ?, ?, ?, ?);",
|
||||
sample.time, sample.zoneId, sample.instanceId, sample.cloneId, sample.players);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<IServerHealth::InstanceSample> MySQLDatabase::GetInstanceSamples(int64_t from, int64_t to, int64_t bucketSeconds, uint32_t zoneId) {
|
||||
std::vector<InstanceSample> samples;
|
||||
const std::string query = "SELECT FLOOR(time / ?) * ? AS bucket, zone_id, instance_id, MAX(clone_id) AS clone_id, MAX(players) AS players FROM server_health_instances "
|
||||
"WHERE time >= ? AND time <= ?" + std::string(zoneId ? " AND zone_id = ?" : "") + " GROUP BY bucket, zone_id, instance_id ORDER BY bucket, zone_id, instance_id;";
|
||||
auto result = zoneId ? ExecuteSelect(query, bucketSeconds, bucketSeconds, from, to, zoneId) : ExecuteSelect(query, bucketSeconds, bucketSeconds, from, to);
|
||||
while (result->next()) {
|
||||
samples.push_back({ result->getInt64("bucket"), result->getUInt("zone_id"), result->getUInt("instance_id"), result->getUInt("clone_id"), result->getUInt("players") });
|
||||
}
|
||||
return samples;
|
||||
}
|
||||
144
dDatabase/GameDatabase/MySQL/Tables/ServerOperations.cpp
Normal file
144
dDatabase/GameDatabase/MySQL/Tables/ServerOperations.cpp
Normal file
@@ -0,0 +1,144 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
#include "GeneralUtils.h"
|
||||
|
||||
namespace {
|
||||
std::string JoinZones(const std::vector<uint32_t>& zones) {
|
||||
std::string text;
|
||||
for (const auto zone : zones) text += (text.empty() ? "" : ",") + std::to_string(zone);
|
||||
return text;
|
||||
}
|
||||
|
||||
std::vector<uint32_t> SplitZones(const std::string& text) {
|
||||
std::vector<uint32_t> zones;
|
||||
for (const auto& part : GeneralUtils::SplitString(text, ',')) {
|
||||
if (const auto zone = GeneralUtils::TryParse<uint32_t>(part)) zones.push_back(*zone);
|
||||
}
|
||||
return zones;
|
||||
}
|
||||
|
||||
std::string Text(PreparedStmtResultSet& result, const char* field) {
|
||||
return result->isNull(field) ? "" : std::string(result->getString(field).c_str());
|
||||
}
|
||||
|
||||
IServerOperations::ScheduledAnnouncement ReadAnnouncement(PreparedStmtResultSet& result) {
|
||||
IServerOperations::ScheduledAnnouncement row;
|
||||
row.id = result->getUInt64("id");
|
||||
row.title = Text(result, "title");
|
||||
row.message = Text(result, "message");
|
||||
row.zones = SplitZones(Text(result, "zones"));
|
||||
row.schedule = Text(result, "schedule");
|
||||
row.startsAt = result->getInt64("starts_at");
|
||||
row.endsAt = result->getInt64("ends_at");
|
||||
row.enabled = result->getInt("enabled") != 0;
|
||||
row.lastSentAt = result->getInt64("last_sent_at");
|
||||
row.sentCount = result->getUInt("sent_count");
|
||||
row.createdAt = result->getInt64("created_at");
|
||||
row.createdBy = Text(result, "created_by");
|
||||
row.updatedAt = result->getInt64("updated_at");
|
||||
row.updatedBy = Text(result, "updated_by");
|
||||
return row;
|
||||
}
|
||||
|
||||
IServerOperations::ScheduledEvent ReadEvent(PreparedStmtResultSet& result) {
|
||||
IServerOperations::ScheduledEvent row;
|
||||
row.id = result->getUInt64("id");
|
||||
row.name = Text(result, "name");
|
||||
row.note = Text(result, "note");
|
||||
row.mode = static_cast<uint8_t>(result->getUInt("mode"));
|
||||
row.schedule = Text(result, "schedule");
|
||||
row.startsAt = result->getInt64("starts_at");
|
||||
row.endsAt = result->getInt64("ends_at");
|
||||
row.priority = result->getInt("priority");
|
||||
row.parts = Text(result, "parts");
|
||||
row.state = static_cast<IServerOperations::eEventState>(result->getInt("state"));
|
||||
row.status = Text(result, "status");
|
||||
row.createdAt = result->getInt64("created_at");
|
||||
row.createdBy = Text(result, "created_by");
|
||||
row.updatedAt = result->getInt64("updated_at");
|
||||
row.updatedBy = Text(result, "updated_by");
|
||||
return row;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<IServerOperations::ScheduledAnnouncement> MySQLDatabase::GetScheduledAnnouncements() {
|
||||
std::vector<ScheduledAnnouncement> rows;
|
||||
auto result = ExecuteSelect("SELECT * FROM scheduled_announcements ORDER BY id;");
|
||||
while (result->next()) rows.push_back(ReadAnnouncement(result));
|
||||
return rows;
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::InsertScheduledAnnouncement(const ScheduledAnnouncement& row) {
|
||||
ExecuteInsert("INSERT INTO scheduled_announcements (title, message, zones, schedule, starts_at, ends_at, enabled, created_at, created_by, updated_at, updated_by) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
row.title, row.message, JoinZones(row.zones), row.schedule, row.startsAt, row.endsAt, row.enabled, row.createdAt, row.createdBy, row.updatedAt, row.updatedBy);
|
||||
auto result = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;");
|
||||
return result->next() ? result->getUInt64("id") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdateScheduledAnnouncement(const ScheduledAnnouncement& row) {
|
||||
ExecuteUpdate("UPDATE scheduled_announcements SET title = ?, message = ?, zones = ?, schedule = ?, starts_at = ?, ends_at = ?, enabled = ?, updated_at = ?, updated_by = ? WHERE id = ?;",
|
||||
row.title, row.message, JoinZones(row.zones), row.schedule, row.startsAt, row.endsAt, row.enabled, row.updatedAt, row.updatedBy, row.id);
|
||||
}
|
||||
|
||||
void MySQLDatabase::MarkAnnouncementSent(uint64_t id, int64_t time) {
|
||||
ExecuteUpdate("UPDATE scheduled_announcements SET last_sent_at = ?, sent_count = sent_count + 1 WHERE id = ?;", time, id);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteScheduledAnnouncement(uint64_t id) {
|
||||
ExecuteDelete("DELETE FROM scheduled_announcements WHERE id = ?;", id);
|
||||
}
|
||||
|
||||
std::vector<IServerOperations::ScheduledEvent> MySQLDatabase::GetScheduledEvents() {
|
||||
std::vector<ScheduledEvent> rows;
|
||||
auto result = ExecuteSelect("SELECT * FROM scheduled_events ORDER BY id;");
|
||||
while (result->next()) rows.push_back(ReadEvent(result));
|
||||
return rows;
|
||||
}
|
||||
|
||||
uint64_t MySQLDatabase::InsertScheduledEvent(const ScheduledEvent& row) {
|
||||
ExecuteInsert("INSERT INTO scheduled_events (feature, name, note, mode, schedule, starts_at, ends_at, priority, parts, state, status, created_at, created_by, updated_at, updated_by) "
|
||||
"VALUES ('', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
||||
row.name, row.note, static_cast<uint32_t>(row.mode), row.schedule, row.startsAt, row.endsAt, row.priority, row.parts, static_cast<uint8_t>(row.state), row.status,
|
||||
row.createdAt, row.createdBy, row.updatedAt, row.updatedBy);
|
||||
auto result = ExecuteSelect("SELECT LAST_INSERT_ID() AS id;");
|
||||
return result->next() ? result->getUInt64("id") : 0;
|
||||
}
|
||||
|
||||
void MySQLDatabase::UpdateScheduledEvent(const ScheduledEvent& row) {
|
||||
ExecuteUpdate("UPDATE scheduled_events SET name = ?, note = ?, mode = ?, schedule = ?, starts_at = ?, ends_at = ?, priority = ?, parts = ?, state = ?, status = ?, "
|
||||
"updated_at = ?, updated_by = ? WHERE id = ?;",
|
||||
row.name, row.note, static_cast<uint32_t>(row.mode), row.schedule, row.startsAt, row.endsAt, row.priority, row.parts, static_cast<uint8_t>(row.state), row.status,
|
||||
row.updatedAt, row.updatedBy, row.id);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteScheduledEvent(uint64_t id) {
|
||||
ExecuteDelete("DELETE FROM scheduled_events WHERE id = ?;", id);
|
||||
}
|
||||
|
||||
std::vector<IServerOperations::ZoneLimit> MySQLDatabase::GetZoneLimits() {
|
||||
std::vector<ZoneLimit> rows;
|
||||
auto result = ExecuteSelect("SELECT * FROM zone_limits ORDER BY zone_id;");
|
||||
while (result->next()) {
|
||||
ZoneLimit row;
|
||||
row.zoneId = result->getUInt("zone_id");
|
||||
if (!result->isNull("soft_cap")) row.softCap = result->getUInt("soft_cap");
|
||||
if (!result->isNull("hard_cap")) row.hardCap = result->getUInt("hard_cap");
|
||||
row.spareInstances = result->getUInt("spare_instances");
|
||||
row.updatedAt = result->getInt64("updated_at");
|
||||
row.updatedBy = Text(result, "updated_by");
|
||||
rows.push_back(std::move(row));
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetZoneLimit(const ZoneLimit& row) {
|
||||
ExecuteInsert("INSERT INTO zone_limits (zone_id, soft_cap, hard_cap, spare_instances, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE soft_cap = VALUES(soft_cap), hard_cap = VALUES(hard_cap), spare_instances = VALUES(spare_instances), "
|
||||
"updated_at = VALUES(updated_at), updated_by = VALUES(updated_by);",
|
||||
row.zoneId, row.softCap, row.hardCap, row.spareInstances, row.updatedAt, row.updatedBy);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteZoneLimit(uint32_t zoneId) {
|
||||
ExecuteDelete("DELETE FROM zone_limits WHERE zone_id = ?;", zoneId);
|
||||
}
|
||||
39
dDatabase/GameDatabase/MySQL/Tables/SlashCommands.cpp
Normal file
39
dDatabase/GameDatabase/MySQL/Tables/SlashCommands.cpp
Normal file
@@ -0,0 +1,39 @@
|
||||
#include "MySQLDatabase.h"
|
||||
|
||||
std::vector<ISlashCommands::SlashCommand> MySQLDatabase::GetSlashCommands() {
|
||||
std::vector<SlashCommand> commands;
|
||||
auto result = ExecuteSelect("SELECT * FROM slash_commands ORDER BY name;");
|
||||
const auto text = [&](const char* field) { return std::string(result->getString(field).c_str()); };
|
||||
while (result->next()) {
|
||||
SlashCommand command;
|
||||
command.name = text("name");
|
||||
command.aliases = GeneralUtils::SplitString(text("aliases"), ',');
|
||||
command.help = text("help");
|
||||
command.info = text("info");
|
||||
command.defaultLevel = static_cast<uint8_t>(result->getInt("default_level"));
|
||||
command.minLevel = static_cast<uint8_t>(result->getInt("min_level"));
|
||||
command.fixed = result->getInt("fixed") != 0;
|
||||
command.clientHandled = result->getInt("client_handled") != 0;
|
||||
command.note = text("note");
|
||||
command.dashboardPermission = text("dashboard_permission");
|
||||
command.targetRule = text("target_rule");
|
||||
command.followsPermission = result->getInt("follows_permission") != 0;
|
||||
commands.push_back(std::move(command));
|
||||
}
|
||||
return commands;
|
||||
}
|
||||
|
||||
void MySQLDatabase::SetSlashCommand(const SlashCommand& command) {
|
||||
std::string aliases;
|
||||
for (const auto& alias : command.aliases) aliases += (aliases.empty() ? "" : ",") + alias;
|
||||
ExecuteInsert(
|
||||
"INSERT INTO slash_commands (name, aliases, help, info, default_level, min_level, fixed, client_handled, note, dashboard_permission, target_rule, follows_permission) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
|
||||
"ON DUPLICATE KEY UPDATE aliases = VALUES(aliases), help = VALUES(help), info = VALUES(info), default_level = VALUES(default_level), "
|
||||
"min_level = VALUES(min_level), fixed = VALUES(fixed), client_handled = VALUES(client_handled), note = VALUES(note), dashboard_permission = VALUES(dashboard_permission), target_rule = VALUES(target_rule), follows_permission = VALUES(follows_permission);",
|
||||
command.name, aliases, command.help, command.info, static_cast<int32_t>(command.defaultLevel),
|
||||
static_cast<int32_t>(command.minLevel), command.fixed, command.clientHandled, command.note, command.dashboardPermission, command.targetRule, command.followsPermission);
|
||||
}
|
||||
|
||||
void MySQLDatabase::DeleteSlashCommand(const std::string& name) {
|
||||
ExecuteDelete("DELETE FROM slash_commands WHERE name = ?;", name);
|
||||
}
|
||||
@@ -71,7 +71,8 @@ void MySQLDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstrea
|
||||
}
|
||||
|
||||
std::optional<IUgc::Model> MySQLDatabase::GetUgcModel(const LWOOBJID ugcId) {
|
||||
auto result = ExecuteSelect("SELECT u.id AS ugcID, lxfml, pc.id AS modelID FROM ugc AS u JOIN properties_contents AS pc ON pc.ugc_id = u.id WHERE u.id = ?", ugcId);
|
||||
// LEFT JOIN: a model that is in someone's inventory rather than placed on a property still exists
|
||||
auto result = ExecuteSelect("SELECT u.id AS ugcID, lxfml, pc.id AS modelID FROM ugc AS u LEFT JOIN properties_contents AS pc ON pc.ugc_id = u.id WHERE u.id = ? LIMIT 1;", ugcId);
|
||||
|
||||
std::optional<IUgc::Model> toReturn = std::nullopt;
|
||||
if (result->next()) {
|
||||
|
||||
Reference in New Issue
Block a user