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:
Aaron Kimbrell
2026-09-26 17:52:10 -05:00
parent 854d02f509
commit e213a7aeff
681 changed files with 88310 additions and 1321 deletions

View File

@@ -0,0 +1,46 @@
#include "SQLiteDatabase.h"
void SQLiteDatabase::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 SQLiteDatabase::DeleteAccountNote(uint64_t id) {
ExecuteDelete("DELETE FROM account_notes WHERE id = ?;", static_cast<int64_t>(id));
}
void SQLiteDatabase::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> SQLiteDatabase::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.eof()) {
notes.push_back({ static_cast<uint64_t>(result.getInt64Field("id")), static_cast<uint32_t>(result.getIntField("account_id")), result.getStringField("kind"),
result.getStringField("text"), result.getStringField("actor"), result.getInt64Field("created_at") });
result.nextRow();
}
return notes;
}
std::optional<IAccountNotes::AccountNote> SQLiteDatabase::GetAccountNote(uint64_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM account_notes WHERE id = ?;", static_cast<int64_t>(id));
if (result.eof()) return std::nullopt;
return AccountNote{ static_cast<uint64_t>(result.getInt64Field("id")), static_cast<uint32_t>(result.getIntField("account_id")), result.getStringField("kind"),
result.getStringField("text"), result.getStringField("actor"), result.getInt64Field("created_at") };
}
uint32_t SQLiteDatabase::GetAccountWarningCount(uint32_t accountId) {
auto [_, result] = ExecuteSelect("SELECT COUNT(*) AS count FROM account_notes WHERE account_id = ? AND kind = 'warning';", accountId);
return result.eof() ? 0 : static_cast<uint32_t>(result.getIntField("count"));
}
std::vector<uint32_t> SQLiteDatabase::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.eof()) { ids.push_back(static_cast<uint32_t>(result.getIntField("id"))); result.nextRow(); }
}
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;
}

View File

@@ -0,0 +1,41 @@
#include "SQLiteDatabase.h"
namespace {
IAccountStrikes::Strike ReadStrike(CppSQLite3Query& result) {
return { static_cast<uint64_t>(result.getInt64Field("id")), static_cast<uint32_t>(result.getIntField("account_id")),
result.getInt64Field("character_id"), result.getStringField("source"), result.getStringField("subject"), result.getStringField("reason"),
static_cast<uint32_t>(result.getIntField("given_by_id")), result.getStringField("given_by"), result.getInt64Field("created_at"),
result.getInt64Field("revoked_at"), result.getStringField("revoked_by"), result.getStringField("revoke_reason") };
}
}
uint64_t SQLiteDatabase::InsertStrike(const Strike& strike) {
ExecuteInsert("INSERT INTO account_strikes (account_id, character_id, source, subject, reason, given_by_id, given_by, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?);",
strike.accountId, strike.characterId, strike.source, strike.subject, strike.reason, strike.givenById, strike.givenBy, strike.createdAt);
auto [_, result] = ExecuteSelect("SELECT last_insert_rowid() AS id;");
return result.eof() ? 0 : static_cast<uint64_t>(result.getInt64Field("id"));
}
std::vector<IAccountStrikes::Strike> SQLiteDatabase::GetStrikes(uint32_t accountId) {
std::vector<Strike> strikes;
auto [_, result] = ExecuteSelect("SELECT * FROM account_strikes WHERE account_id = ? ORDER BY id DESC;", accountId);
for (; !result.eof(); result.nextRow()) strikes.push_back(ReadStrike(result));
return strikes;
}
std::optional<IAccountStrikes::Strike> SQLiteDatabase::GetStrike(uint64_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM account_strikes WHERE id = ?;", static_cast<int64_t>(id));
if (result.eof()) return std::nullopt;
return ReadStrike(result);
}
void SQLiteDatabase::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, static_cast<int64_t>(id));
}
uint32_t SQLiteDatabase::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.eof() ? 0 : static_cast<uint32_t>(result.getIntField("count"));
}

View File

@@ -1,7 +1,9 @@
#include "SQLiteDatabase.h"
#include "GeneralUtils.h"
#include "eGameMasterLevel.h"
#include "Database.h"
#include "json.hpp"
std::optional<IAccounts::Info> SQLiteDatabase::GetAccountInfo(const std::string_view username) {
auto [_, result] = ExecuteSelect("SELECT * FROM accounts WHERE name = ? LIMIT 1", username);
@@ -18,6 +20,8 @@ std::optional<IAccounts::Info> SQLiteDatabase::GetAccountInfo(const std::string_
toReturn.locked = result.getIntField("locked");
toReturn.playKeyId = result.getIntField("play_key_id");
toReturn.muteExpire = static_cast<uint64_t>(result.getInt64Field("mute_expire"));
toReturn.banExpires = result.getInt64Field("ban_expires");
toReturn.banReason = result.fieldIsNull("ban_reason") ? "" : result.getStringField("ban_reason");
return toReturn;
}
@@ -34,8 +38,8 @@ void SQLiteDatabase::UpdateAccountPassword(const uint32_t accountId, const std::
ExecuteUpdate("UPDATE accounts SET password = ? WHERE id = ?;", bcryptpassword, accountId);
}
void SQLiteDatabase::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 SQLiteDatabase::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 SQLiteDatabase::UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) {
@@ -48,3 +52,178 @@ uint32_t SQLiteDatabase::GetAccountCount() {
return res.getIntField("count");
}
void SQLiteDatabase::RecordFailedAttempt(const uint32_t accountId) {
ExecuteUpdate("UPDATE accounts SET failed_attempts = failed_attempts + 1 WHERE id = ?;", accountId);
}
void SQLiteDatabase::ClearFailedAttempts(const uint32_t accountId) {
ExecuteUpdate("UPDATE accounts SET failed_attempts = 0, lockout_time = NULL, last_login = CURRENT_TIMESTAMP WHERE id = ?;", accountId);
}
void SQLiteDatabase::SetLockout(const uint32_t accountId, const int64_t lockoutUntil) {
ExecuteUpdate("UPDATE accounts SET lockout_time = datetime(NULLIF(?, 0), 'unixepoch'), failed_attempts = 0 WHERE id = ?;", lockoutUntil, accountId);
}
bool SQLiteDatabase::IsLockedOut(const uint32_t accountId) {
// lockout_time is stored as UTC text by datetime(?, 'unixepoch'), the same format datetime('now') gives
auto [_, result] = ExecuteSelect("SELECT 1 AS locked FROM accounts WHERE id = ? AND lockout_time IS NOT NULL AND lockout_time > datetime('now');", accountId);
return !result.eof();
}
uint32_t SQLiteDatabase::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.eof() ? 0 : static_cast<uint32_t>(result.getIntField("count"));
}
void SQLiteDatabase::SetAccountLocked(const uint32_t accountId, const bool locked) {
ExecuteUpdate("UPDATE accounts SET locked = ? WHERE id = ?;", locked, accountId);
}
uint8_t SQLiteDatabase::GetFailedAttempts(const uint32_t accountId) {
auto [_, result] = ExecuteSelect("SELECT failed_attempts FROM accounts WHERE id = ?;", accountId);
if (result.eof()) {
return 0;
}
return result.getIntField("failed_attempts");
}
nlohmann::json SQLiteDatabase::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 '%' || ? || '%' OR id = ? OR id IN (SELECT account_id FROM charinfo WHERE name LIKE '%' || ? || '%'))";
}
// 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 ? OFFSET ?;";
// Get total count
std::string totalCountQuery = "SELECT COUNT(*) as count FROM accounts;";
auto [_, totalCountResult] = ExecuteSelect(totalCountQuery);
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
// Get filtered count
uint32_t filteredRecords = totalRecords;
if (!search.empty()) {
std::string filteredCountQuery = "SELECT COUNT(*) as count FROM accounts WHERE (name LIKE '%' || ? || '%' OR id = ? OR id IN (SELECT account_id FROM charinfo WHERE name LIKE '%' || ? || '%'));";
auto [__, filteredCountResult] = ExecuteSelect(filteredCountQuery, search, searchId, search);
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
}
// Execute main query
auto [stmt, result] = !search.empty() ?
ExecuteSelect(mainQuery, search, searchId, search, length, start) :
ExecuteSelect(mainQuery, length, start);
// Build response JSON
nlohmann::json accountsArray = nlohmann::json::array();
while (!result.eof()) {
nlohmann::json account = {
{"id", result.getIntField("id")},
{"name", result.getStringField("name")},
{"banned", result.getIntField("banned") != 0},
{"locked", result.getIntField("locked") != 0},
{"gm_level", result.getIntField("gm_level")},
{"mute_expire", result.getInt64Field("mute_expire")},
{"created_at", result.getStringField("created_at")}
};
accountsArray.push_back(account);
result.nextRow();
}
nlohmann::json response = {
{"draw", 1},
{"recordsTotal", totalRecords},
{"recordsFiltered", filteredRecords},
{"data", accountsArray}
};
return response;
}
nlohmann::json SQLiteDatabase::GetAccountById(uint32_t accountId) {
try {
auto [_, result] = ExecuteSelect("SELECT * FROM accounts WHERE id = ? LIMIT 1;", accountId);
if (result.eof()) {
return nlohmann::json{{"error", "Account not found"}};
}
nlohmann::json account = {
{"id", result.getIntField("id")},
{"name", result.getStringField("name")},
{"banned", result.getIntField("banned") != 0},
{"locked", result.getIntField("locked") != 0},
{"ban_expires", result.getInt64Field("ban_expires")},
{"ban_reason", result.fieldIsNull("ban_reason") ? "" : result.getStringField("ban_reason")},
{"gm_level", result.getIntField("gm_level")},
{"mute_expire", result.getInt64Field("mute_expire")},
{"created_at", result.getStringField("created_at")}
};
return account;
} catch (const CppSQLite3Exception& e) {
LOG_DEBUG("SQLite Error: %s", e.errorMessage());
return nlohmann::json{{"error", "Database error"}};
}
}
void SQLiteDatabase::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.eof()) {
characters.push_back(result.getInt64Field("id"));
result.nextRow();
}
}
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 SQLiteDatabase::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.eof()) {
chars.push_back({
{"id", std::to_string(result.getInt64Field("id"))},
{"name", result.getStringField("name")},
{"last_login", result.getInt64Field("last_login")}
});
result.nextRow();
}
return chars;
}

View File

@@ -1,6 +1,66 @@
#include "SQLiteDatabase.h"
#include "GeneralUtils.h"
#include "json.hpp"
void SQLiteDatabase::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 SQLiteDatabase::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 '%' || ? || '%' 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 ? OFFSET ?;";
// 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 ? OFFSET ?) 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.eof() ? 0 : totalCountResult.getIntField("count");
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 '%' || ? || '%' OR a.character_id = ?);", search, searchId);
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
}
auto [stmt, result] = !search.empty()
? ExecuteSelect(mainQuery, search, searchId, length, start)
: ExecuteSelect(mainQuery, length, start);
nlohmann::json dataArray = nlohmann::json::array();
while (!result.eof()) {
dataArray.push_back({
{"id", result.getIntField("id")},
{"character_name", result.getStringField("character_name")},
{"activity", result.getIntField("activity")},
{"time", result.getInt64Field("time")},
{"map_id", result.getIntField("map_id")}
});
result.nextRow();
}
return nlohmann::json({{"draw", 0}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", dataArray}}).dump();
}
uint32_t SQLiteDatabase::GetActivityLogCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM activity_log;");
if (res.eof()) return 0;
return res.getIntField("count");
}

View File

@@ -0,0 +1,305 @@
#include "SQLiteDatabase.h"
#include <ctime>
namespace {
IDashboardAdmin::Webhook WebhookRow(CppSQLite3Query& result) {
IDashboardAdmin::Webhook webhook;
webhook.id = static_cast<uint32_t>(result.getIntField("id"));
webhook.name = result.getStringField("name");
webhook.url = result.getStringField("url");
webhook.format = result.getStringField("format");
webhook.events = result.getStringField("events");
webhook.secret = result.getStringField("secret");
webhook.enabled = result.getIntField("enabled") != 0;
webhook.createdAt = result.getInt64Field("created_at");
webhook.lastSentAt = result.getInt64Field("last_sent_at");
webhook.lastStatus = result.getIntField("last_status");
webhook.lastError = result.fieldIsNull("last_error") ? "" : result.getStringField("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;
const char* conflict; // the primary key, for ON CONFLICT
std::vector<const char*> values;
};
const std::vector<LedgerTable> DAILY_TABLES{
{ "economy_currency_daily", "character_id, source", "day, character_id, source", { "gained", "spent" } },
{ "economy_uscore_daily", "character_id, source", "day, character_id, source", { "gained", "lost" } },
{ "economy_items_daily", "lot, source, gm", "day, lot, source, gm", { "created", "destroyed" } },
{ "player_stats_daily", "zone, clone_id, stat, gm", "day, zone, clone_id, stat, gm", { "amount" } },
};
const LedgerTable MAP_TABLE{ "map_events_daily", "zone, clone_id, kind, lot, cell_x, cell_z", "zone, kind, day, clone_id, lot, cell_x, cell_z", { "events", "quantity" } };
}
std::vector<IDashboardAdmin::Webhook> SQLiteDatabase::GetWebhooks() {
auto [_, result] = ExecuteSelect("SELECT * FROM dashboard_webhooks ORDER BY id;");
std::vector<Webhook> webhooks;
while (!result.eof()) {
webhooks.push_back(WebhookRow(result));
result.nextRow();
}
return webhooks;
}
std::optional<IDashboardAdmin::Webhook> SQLiteDatabase::GetWebhook(uint32_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM dashboard_webhooks WHERE id = ?;", id);
if (result.eof()) return std::nullopt;
return WebhookRow(result);
}
void SQLiteDatabase::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 SQLiteDatabase::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 SQLiteDatabase::DeleteWebhook(uint32_t id) {
ExecuteDelete("DELETE FROM dashboard_webhooks WHERE id = ?;", id);
}
void SQLiteDatabase::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> SQLiteDatabase::GetDashboardState(const std::string& name) {
auto [_, result] = ExecuteSelect("SELECT value FROM dashboard_state WHERE name = ?;", name);
if (result.eof()) return std::nullopt;
return std::string(result.getStringField("value"));
}
void SQLiteDatabase::SetDashboardState(const std::string& name, const std::string& value) {
ExecuteInsert("INSERT INTO dashboard_state (name, value) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET value = excluded.value;", name, value);
}
void SQLiteDatabase::DeleteDashboardState(const std::string& name) {
ExecuteDelete("DELETE FROM dashboard_state WHERE name = ?;", name);
}
std::string SQLiteDatabase::GetDashboardPreferences(uint32_t accountId) {
auto [_, result] = ExecuteSelect("SELECT prefs FROM dashboard_preferences WHERE account_id = ?;", accountId);
if (result.eof()) return "{}";
return std::string(result.getStringField("prefs"));
}
void SQLiteDatabase::SetDashboardPreferences(uint32_t accountId, const std::string& prefs) {
ExecuteInsert("INSERT INTO dashboard_preferences (account_id, prefs) VALUES (?, ?) ON CONFLICT(account_id) DO UPDATE SET prefs = excluded.prefs;", accountId, prefs);
}
bool SQLiteDatabase::InsertEconomyFlag(const EconomyFlag& flag) {
return ExecuteUpdate(
"INSERT OR 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 SQLiteDatabase::GetEconomyFlagsTable(uint32_t start, uint32_t length, int32_t status) {
const std::string where = status >= 0 ? " WHERE f.status = ?" : "";
const auto count = [&]() {
auto [_, result] = status >= 0 ? ExecuteSelect("SELECT COUNT(*) AS count FROM economy_flags f" + where + ";", status)
: ExecuteSelect("SELECT COUNT(*) AS count FROM economy_flags f;");
return result.eof() ? 0 : result.getIntField("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.eof()) {
data.push_back({
{"id", result.getInt64Field("id")},
{"created_at", result.getInt64Field("created_at")},
{"day", result.getIntField("day")},
{"kind", result.getIntField("kind")},
{"character_id", std::to_string(result.getInt64Field("character_id"))},
{"character_name", result.fieldIsNull("character_name") ? "" : result.getStringField("character_name")},
{"lot", result.getIntField("lot")},
{"item_id", std::to_string(result.getInt64Field("item_id"))},
{"value", result.getInt64Field("value")},
{"baseline", result.getInt64Field("baseline")},
{"details", result.fieldIsNull("details") ? "" : result.getStringField("details")},
{"status", result.getIntField("status")},
{"reviewer_name", result.fieldIsNull("reviewer_name") ? "" : result.getStringField("reviewer_name")},
{"reviewed_at", result.getInt64Field("reviewed_at")},
{"note", result.fieldIsNull("note") ? "" : result.getStringField("note")}
});
result.nextRow();
}
return { {"draw", 0}, {"recordsTotal", count}, {"recordsFiltered", count}, {"data", data} };
}
void SQLiteDatabase::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 SQLiteDatabase::GetOpenEconomyFlagCount() {
auto [_, result] = ExecuteSelect("SELECT COUNT(*) AS count FROM economy_flags WHERE status = 0;");
return result.eof() ? 0 : static_cast<uint32_t>(result.getIntField("count"));
}
std::vector<std::pair<LWOOBJID, int64_t>> SQLiteDatabase::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.eof()) {
rows.emplace_back(result.getInt64Field("character_id"), result.getInt64Field("gained"));
result.nextRow();
}
return rows;
}
std::map<LOT, int64_t> SQLiteDatabase::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.eof()) {
totals[result.getIntField("lot")] = result.getInt64Field("created");
result.nextRow();
}
return totals;
}
uint32_t SQLiteDatabase::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.eof()) {
days.push_back(static_cast<uint32_t>(result.getIntField("day")));
result.nextRow();
}
}
std::string values, sums;
for (const auto* value : table.values) {
values += std::string(", ") + value;
sums += std::string(sums.empty() ? "" : ", ") + value + " = " + value + " + excluded." + value;
}
for (const auto day : days) {
const auto month = MonthStartDay(day);
if (month == day) continue;
// One short transaction per day, so the world servers' saves never wait long for the write lock
DatabaseTransaction transaction(*this);
ExecuteInsert(std::string("INSERT INTO ") + table.name + " (day, " + table.keys + values + ") SELECT ?, " + table.keys + values +
" FROM " + table.name + " WHERE day = ? ON CONFLICT(" + table.conflict + ") DO UPDATE SET " + 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 SQLiteDatabase::PruneTransfers(int64_t beforeTime) {
return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM economy_transfers WHERE time < ?;", beforeTime));
}
IDashboardAdmin::Totp SQLiteDatabase::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.eof()) return totp;
totp.encryptedSecret = result.fieldIsNull("totp_secret") ? "" : result.getStringField("totp_secret");
totp.enabledAt = result.getInt64Field("totp_enabled_at");
totp.lastStep = result.getInt64Field("totp_last_step");
return totp;
}
void SQLiteDatabase::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 SQLiteDatabase::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 SQLiteDatabase::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 SQLiteDatabase::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 SQLiteDatabase::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.eof() ? 0 : static_cast<uint32_t>(result.getIntField("count"));
}
nlohmann::json SQLiteDatabase::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.eof()) {
rows.push_back({
{"id", result.getIntField("id")}, {"body", result.getStringField("body")}, {"client_version", result.getStringField("client_version")},
{"other_player_id", result.getStringField("other_player_id")}, {"selection", result.getStringField("selection")},
{"reporter_id", std::to_string(result.getInt64Field("reporter_id"))},
{"reporter_name", result.fieldIsNull("reporter_name") ? "" : result.getStringField("reporter_name")}
});
result.nextRow();
}
return rows;
}
uint32_t SQLiteDatabase::GetMaxBugReportId() {
auto [_, result] = ExecuteSelect("SELECT COALESCE(MAX(id), 0) AS id FROM bug_reports;");
return result.eof() ? 0 : static_cast<uint32_t>(result.getIntField("id"));
}
nlohmann::json SQLiteDatabase::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));
auto [_, countResult] = ExecuteSelect("SELECT COUNT(*) AS count FROM economy_transfers t" + where + ";");
const auto total = countResult.eof() ? 0 : countResult.getIntField("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.eof()) {
data.push_back({
{"time", result.getInt64Field("time")}, {"method", result.getIntField("method")},
{"item_id", std::to_string(result.getInt64Field("item_id"))}, {"lot", result.getIntField("lot")},
{"count", result.getIntField("count")}, {"coins", result.getInt64Field("coins")},
{"from_character", std::to_string(result.getInt64Field("from_character"))},
{"from_name", result.fieldIsNull("from_name") ? "" : result.getStringField("from_name")},
{"to_character", std::to_string(result.getInt64Field("to_character"))},
{"to_name", result.fieldIsNull("to_name") ? "" : result.getStringField("to_name")}
});
result.nextRow();
}
return { {"draw", 0}, {"recordsTotal", total}, {"recordsFiltered", total}, {"data", data} };
}
uint32_t SQLiteDatabase::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 < datetime(?, 'unixepoch');", 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;
}

View File

@@ -0,0 +1,76 @@
#include "SQLiteDatabase.h"
namespace {
std::string Text(CppSQLite3Query& result, const char* field) {
return result.fieldIsNull(field) ? "" : result.getStringField(field);
}
IAiSuggestions::AiSuggestion SuggestionRow(CppSQLite3Query& result) {
IAiSuggestions::AiSuggestion row;
row.id = static_cast<uint64_t>(result.getInt64Field("id"));
row.kind = Text(result, "kind");
row.itemId = result.getInt64Field("item_id");
row.fingerprint = Text(result, "fingerprint");
row.requestedById = static_cast<uint32_t>(result.getInt64Field("requested_by_id"));
row.requestedBy = Text(result, "requested_by");
row.createdAt = result.getInt64Field("created_at");
row.model = Text(result, "model");
row.inputTokens = static_cast<uint32_t>(result.getInt64Field("input_tokens"));
row.outputTokens = static_cast<uint32_t>(result.getInt64Field("output_tokens"));
row.status = static_cast<IAiSuggestions::eAiStatus>(result.getIntField("status"));
row.suggestion = Text(result, "suggestion");
row.error = Text(result, "error");
row.context = Text(result, "context");
return row;
}
}
uint64_t SQLiteDatabase::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_rowid() AS id;");
return result.eof() ? 0 : static_cast<uint64_t>(result.getInt64Field("id"));
}
std::vector<IAiSuggestions::AiSuggestion> SQLiteDatabase::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;
for (; !result.eof(); result.nextRow()) rows.push_back(SuggestionRow(result));
return rows;
}
std::optional<IAiSuggestions::AiSuggestion> SQLiteDatabase::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.eof()) return std::nullopt;
return SuggestionRow(result);
}
IAiSuggestions::AiUsage SQLiteDatabase::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.eof()) return {};
return { static_cast<uint32_t>(result.getInt64Field("requests")), static_cast<uint64_t>(result.getInt64Field("input")),
static_cast<uint64_t>(result.getInt64Field("output")) };
}
std::optional<IAiSuggestions::EconomyFlagRow> SQLiteDatabase::GetEconomyFlagRow(uint64_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM economy_flags WHERE id = ?;", static_cast<int64_t>(id));
if (result.eof()) return std::nullopt;
EconomyFlagRow row;
row.id = static_cast<uint64_t>(result.getInt64Field("id"));
row.createdAt = result.getInt64Field("created_at");
row.day = static_cast<uint32_t>(result.getIntField("day"));
row.kind = static_cast<uint8_t>(result.getIntField("kind"));
row.characterId = result.getInt64Field("character_id");
row.lot = result.getIntField("lot");
row.itemId = result.getInt64Field("item_id");
row.value = result.getInt64Field("value");
row.baseline = result.getInt64Field("baseline");
row.details = Text(result, "details");
row.status = static_cast<uint8_t>(result.getIntField("status"));
row.note = Text(result, "note");
return row;
}

View File

@@ -0,0 +1,76 @@
#include "SQLiteDatabase.h"
#include "GeneralUtils.h"
#include "json.hpp"
#include <ctime>
void SQLiteDatabase::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 SQLiteDatabase::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 '%' || ? || '%' OR account_name LIKE '%' || ? || '%' OR description LIKE '%' || ? || '%' 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 ? OFFSET ?;";
auto [__, totalCountResult] = ExecuteSelect("SELECT COUNT(*) as count FROM audit_log;");
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
uint32_t filteredRecords = totalRecords;
if (!search.empty()) {
auto [___, filteredCountResult] = ExecuteSelect("SELECT COUNT(*) as count FROM audit_log WHERE (action LIKE '%' || ? || '%' OR account_name LIKE '%' || ? || '%' OR description LIKE '%' || ? || '%' OR account_id = ? OR target_account_id = ? OR target_character_id = ?);", search, search, search, searchId, searchId, searchId);
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
}
auto [stmt, result] = !search.empty()
? ExecuteSelect(mainQuery, search, search, search, searchId, searchId, searchId, length, start)
: ExecuteSelect(mainQuery, length, start);
nlohmann::json dataArray = nlohmann::json::array();
while (!result.eof()) {
dataArray.push_back({
{"id", result.getIntField("id")},
{"account_name", result.getStringField("account_name")},
{"action", result.getStringField("action")},
{"description", result.getStringField("description")},
{"timestamp", result.getInt64Field("timestamp")},
{"target_account_id", result.getIntField("target_account_id")},
{"target_account_name", std::string(result.fieldIsNull("target_account_name") ? "" : result.getStringField("target_account_name"))},
{"target_character_id", std::to_string(result.getInt64Field("target_character_id"))},
{"target_character_name", std::string(result.fieldIsNull("target_character_name") ? "" : result.getStringField("target_character_name"))}
});
result.nextRow();
}
nlohmann::json response = {
{"draw", 0},
{"recordsTotal", totalRecords},
{"recordsFiltered", filteredRecords},
{"data", dataArray}
};
return response.dump();
}

View File

@@ -4,3 +4,16 @@ void SQLiteDatabase::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 SQLiteDatabase::DeleteBugReport(const uint32_t id) {
ExecuteUpdate("DELETE FROM bug_reports WHERE id = ?;", id);
}
uint32_t SQLiteDatabase::GetBugReportCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM bug_reports;");
if (res.eof()) return 0;
return res.getIntField("count");
}

View File

@@ -1,6 +1,26 @@
set(DDATABASES_DATABASES_SQLITE_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"

View File

@@ -1,4 +1,5 @@
#include "SQLiteDatabase.h"
#include "json.hpp"
std::vector<std::string> SQLiteDatabase::GetApprovedCharacterNames() {
auto [_, result] = ExecuteSelect("SELECT name FROM charinfo;");
@@ -55,6 +56,13 @@ std::vector<LWOOBJID> SQLiteDatabase::GetAccountCharacterIds(const LWOOBJID acco
return toReturn;
}
uint32_t SQLiteDatabase::GetCharacterCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM charinfo;");
if (res.eof()) return 0;
return res.getIntField("count");
}
void SQLiteDatabase::InsertNewCharacter(const ICharInfo::Info info) {
ExecuteInsert(
"INSERT INTO `charinfo`(`id`, `account_id`, `name`, `pending_name`, `needs_rename`, `last_login`, `prop_clone_id`) VALUES (?,?,?,?,?,?,(SELECT IFNULL(MAX(`prop_clone_id`), 0) + 1 FROM `charinfo`))",
@@ -83,3 +91,23 @@ bool SQLiteDatabase::IsNameInUse(const std::string_view name) {
return !result.eof();
}
nlohmann::json SQLiteDatabase::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.eof()) {
return nlohmann::json({{"error", "Character not found"}});
}
return nlohmann::json({
{"id", std::to_string(result.getInt64Field("id"))},
{"name", result.getStringField("name")},
{"pending_name", result.getStringField("pending_name")},
{"needs_rename", result.getIntField("needs_rename") != 0},
{"account_id", result.getIntField("account_id")},
{"account_name", result.getStringField("account_name")},
{"last_login", result.getInt64Field("last_login")},
{"prop_clone_id", result.getInt64Field("prop_clone_id")},
{"permission_map", result.getIntField("permission_map")}
});
}

View File

@@ -17,3 +17,76 @@ void SQLiteDatabase::UpdateCharacterXml(const LWOOBJID charId, const std::string
void SQLiteDatabase::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 SQLiteDatabase::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 '%' || ? || '%' OR c.id = ? OR a.name LIKE '%' || ? || '%')";
}
// 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 ? OFFSET ?;";
// Get total count
std::string totalCountQuery = "SELECT COUNT(*) as count FROM charinfo;";
auto [__, totalCountResult] = ExecuteSelect(totalCountQuery);
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
// 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 '%' || ? || '%' OR c.id = ? OR a.name LIKE '%' || ? || '%');";
auto [___, filteredCountResult] = ExecuteSelect(filteredCountQuery, search, searchId, search);
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
}
// Execute main query
auto [stmt, result] = !search.empty() ?
ExecuteSelect(mainQuery, search, searchId, search, length, start) :
ExecuteSelect(mainQuery, length, start);
// Build response JSON
nlohmann::json charactersArray = nlohmann::json::array();
while (!result.eof()) {
nlohmann::json character = {
{"id", std::to_string(result.getInt64Field("id"))},
{"name", result.getStringField("name")},
{"account_name", result.getStringField("account_name")},
{"last_login", result.getInt64Field("last_login")}
};
charactersArray.push_back(character);
result.nextRow();
}
nlohmann::json response = {
{"draw", 0},
{"recordsTotal", totalRecords},
{"recordsFiltered", filteredRecords},
{"data", charactersArray}
};
return response.dump();
}

View File

@@ -0,0 +1,61 @@
#include "SQLiteDatabase.h"
#include <sstream>
namespace {
ICharacterSnapshots::CharacterSnapshot Row(CppSQLite3Query& result, bool withXml) {
ICharacterSnapshots::CharacterSnapshot snapshot;
snapshot.id = static_cast<uint64_t>(result.getInt64Field("id"));
snapshot.characterId = result.getInt64Field("character_id");
snapshot.takenAt = result.getInt64Field("taken_at");
snapshot.reason = result.getStringField("reason");
snapshot.actor = result.getStringField("actor");
snapshot.size = static_cast<uint32_t>(result.getInt64Field("size"));
snapshot.hash = result.getStringField("hash");
if (withXml) {
int length = 0;
const auto* blob = result.getBlobField("xml", length);
snapshot.compressed.assign(reinterpret_cast<const char*>(blob), length);
}
return snapshot;
}
}
void SQLiteDatabase::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> SQLiteDatabase::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.eof()) {
snapshots.push_back(Row(result, false));
result.nextRow();
}
return snapshots;
}
std::optional<ICharacterSnapshots::CharacterSnapshot> SQLiteDatabase::GetCharacterSnapshot(uint64_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM character_snapshots WHERE id = ?;", static_cast<int64_t>(id));
if (result.eof()) return std::nullopt;
return Row(result, true);
}
std::map<LWOOBJID, std::string> SQLiteDatabase::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.eof()) {
hashes[result.getInt64Field("character_id")] = result.getStringField("hash");
result.nextRow();
}
return hashes;
}
uint32_t SQLiteDatabase::PruneCharacterSnapshots(int64_t beforeTime, uint32_t keep) {
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) WHERE position > ?);",
beforeTime, keep));
}

View File

@@ -0,0 +1,48 @@
#include "SQLiteDatabase.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 '%' || ? || '%' ESCAPE '!' OR sender_name LIKE '%' || ? || '%' ESCAPE '!' OR recipient_name LIKE '%' || ? || '%' ESCAPE '!'))";
where += q.blockedOnly ? " AND (? <> 0 AND blocked = 1)" : " AND (? = 0)";
return where;
}
}
uint64_t SQLiteDatabase::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_rowid() AS id;"); // this connection's insert, not another server's
return last.eof() ? 0 : static_cast<uint64_t>(last.getInt64Field("id"));
}
std::vector<IChatLog::ChatMessage> SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) {
messages.push_back({ static_cast<uint64_t>(result.getInt64Field("id")), result.getInt64Field("time"), result.getStringField("channel"),
result.getInt64Field("sender_id"), result.getStringField("sender_name"), static_cast<uint32_t>(result.getIntField("account_id")),
result.getInt64Field("recipient_id"), result.getStringField("recipient_name"), static_cast<uint32_t>(result.getIntField("zone_id")),
static_cast<uint32_t>(result.getIntField("instance_id")), static_cast<uint32_t>(result.getIntField("clone_id")), result.getStringField("message"),
result.getIntField("blocked") != 0 });
}
return messages;
}
uint64_t SQLiteDatabase::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.eof() ? 0 : static_cast<uint64_t>(result.getInt64Field("count"));
}

View File

@@ -1,5 +1,59 @@
#include "SQLiteDatabase.h"
#include "GeneralUtils.h"
#include <ctime>
#include "json.hpp"
void SQLiteDatabase::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 SQLiteDatabase::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 '%' || ? || '%' OR cl.command LIKE '%' || ? || '%' 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 ? OFFSET ?;";
// 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 ? OFFSET ?) 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.eof() ? 0 : totalCountResult.getIntField("count");
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 '%' || ? || '%' OR cl.command LIKE '%' || ? || '%' OR cl.character_id = ?);", search, search, searchId);
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
}
auto [stmt, result] = !search.empty()
? ExecuteSelect(mainQuery, search, search, searchId, length, start)
: ExecuteSelect(mainQuery, length, start);
nlohmann::json dataArray = nlohmann::json::array();
while (!result.eof()) {
dataArray.push_back({
{"id", result.getIntField("id")},
{"character_name", result.getStringField("character_name")},
{"command", result.getStringField("command")},
{"character_id", std::to_string(result.getInt64Field("character_id"))},
{"time", result.getInt64Field("time")} // 0: logged before times were recorded
});
result.nextRow();
}
return nlohmann::json({{"draw", 0}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", dataArray}}).dump();
}

View File

@@ -0,0 +1,378 @@
#include "SQLiteDatabase.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(CppSQLite3Query& result, const char* field) {
return result.fieldIsNull(field) ? "" : result.getStringField(field);
}
}
std::string SQLiteDatabase::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 '%' || ? || '%' OR b.other_player_id LIKE '%' || ? || '%' OR c.name LIKE '%' || ? || '%' 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.eof() ? 0 : totalResult.getIntField("count");
uint32_t filteredRecords = totalRecords;
if (!search.empty()) {
auto [__, filteredResult] = ExecuteSelect("SELECT COUNT(*) AS count" + from + where + ";", search, search, search, searchId, searchId);
filteredRecords = filteredResult.eof() ? 0 : filteredResult.getIntField("count");
}
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 ? OFFSET ?;";
auto [stmt, result] = !search.empty()
? ExecuteSelect(query, search, search, search, searchId, searchId, length, start)
: ExecuteSelect(query, length, start);
nlohmann::json data = nlohmann::json::array();
while (!result.eof()) {
data.push_back({
{"id", result.getIntField("id")},
{"body", result.getStringField("body")},
{"client_version", result.getStringField("client_version")},
{"other_player_id", result.getStringField("other_player_id")},
{"selection", result.getStringField("selection")},
{"submitted", result.getStringField("submitted")},
{"reporter_id", std::to_string(result.getInt64Field("reporter_id"))},
{"reporter_name", NullableString(result, "reporter_name")},
{"resolved", !result.fieldIsNull("resolved_time")}
});
result.nextRow();
}
return nlohmann::json({ {"draw", 0}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", data} }).dump();
}
nlohmann::json SQLiteDatabase::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.eof()) return nlohmann::json{ {"error", "Bug report not found"} };
return nlohmann::json{
{"id", result.getIntField("id")},
{"body", result.getStringField("body")},
{"client_version", result.getStringField("client_version")},
{"other_player_id", result.getStringField("other_player_id")},
{"selection", result.getStringField("selection")},
{"submitted", result.getStringField("submitted")},
{"reporter_id", std::to_string(result.getInt64Field("reporter_id"))},
{"reporter_name", NullableString(result, "reporter_name")},
{"resolved", !result.fieldIsNull("resolved_time")},
{"resolved_time", NullableString(result, "resolved_time")},
{"resolution", NullableString(result, "resolution")},
{"resolver_name", NullableString(result, "resolver_name")}
};
}
void SQLiteDatabase::ResolveBugReport(const uint32_t id, const uint32_t resolverAccountId, const std::string_view resolution) {
ExecuteUpdate("UPDATE bug_reports SET resolved_time = datetime('now'), resoleved_by_id = ?, resolution = ? WHERE id = ?;", resolverAccountId, resolution, id);
}
std::string SQLiteDatabase::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 '%' || ? || '%' OR p.notes LIKE '%' || ? || '%' OR p.id = ? OR p.id IN (SELECT play_key_id FROM accounts WHERE name LIKE '%' || ? || '%'))";
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.eof() ? 0 : totalResult.getIntField("count");
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.eof() ? 0 : filteredResult.getIntField("count");
}
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 ? OFFSET ?;";
auto [stmt, result] = !search.empty()
? ExecuteSelect(query, search, search, searchId, search, length, start)
: ExecuteSelect(query, length, start);
nlohmann::json data = nlohmann::json::array();
while (!result.eof()) {
data.push_back({
{"id", result.getIntField("id")},
{"key_string", result.getStringField("key_string")},
{"key_uses", result.getIntField("key_uses")},
{"times_used", result.getIntField("times_used")},
{"created_at", result.getStringField("created_at")},
{"active", result.getIntField("active") != 0},
{"notes", NullableString(result, "notes")}
});
result.nextRow();
}
return nlohmann::json({ {"draw", 0}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", data} }).dump();
}
nlohmann::json SQLiteDatabase::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.eof()) return nlohmann::json{ {"error", "Play key not found"} };
nlohmann::json key{
{"id", result.getIntField("id")},
{"key_string", result.getStringField("key_string")},
{"key_uses", result.getIntField("key_uses")},
{"created_at", result.getStringField("created_at")},
{"active", result.getIntField("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.eof()) {
accounts.push_back({
{"id", accountResult.getIntField("id")},
{"name", accountResult.getStringField("name")},
{"created_at", accountResult.getStringField("created_at")}
});
accountResult.nextRow();
}
key["times_used"] = accounts.size();
key["accounts"] = accounts;
return key;
}
void SQLiteDatabase::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 SQLiteDatabase::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>> SQLiteDatabase::GetCharacterIdsAndNames() {
auto [_, result] = ExecuteSelect("SELECT id, name FROM charinfo ORDER BY name;");
std::vector<std::pair<LWOOBJID, std::string>> characters;
while (!result.eof()) {
characters.emplace_back(result.getInt64Field("id"), result.getStringField("name"));
result.nextRow();
}
return characters;
}
nlohmann::json SQLiteDatabase::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.eof() ? 0 : countResult.getIntField("count");
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 ? OFFSET ?;", length, start);
nlohmann::json data = nlohmann::json::array();
while (!result.eof()) {
data.push_back({
{"id", std::to_string(result.getInt64Field("id"))},
{"name", result.getStringField("name")},
{"pending_name", result.getStringField("pending_name")},
{"account_id", result.getIntField("account_id")},
{"account_name", NullableString(result, "account_name")}
});
result.nextRow();
}
return nlohmann::json{ {"draw", 0}, {"recordsTotal", total}, {"recordsFiltered", total}, {"data", data} };
}
void SQLiteDatabase::SetCharacterPermissionMap(const LWOOBJID characterId, const uint64_t permissionMap) {
ExecuteUpdate("UPDATE charinfo SET permission_map = ? WHERE id = ?;", permissionMap, characterId);
}
IDashboardStats::Snapshot SQLiteDatabase::GetDashboardSnapshot() {
IDashboardStats::Snapshot snapshot;
auto [_, result] = ExecuteSelect(SNAPSHOT_QUERY);
if (result.eof()) return snapshot;
snapshot.accounts = result.getInt64Field("accounts");
snapshot.accountsMaxId = result.getInt64Field("accounts_max_id");
snapshot.characters = result.getInt64Field("characters");
snapshot.pendingNames = result.getInt64Field("pending_names");
snapshot.properties = result.getInt64Field("properties");
snapshot.pendingProperties = result.getInt64Field("pending_properties");
snapshot.playKeys = result.getInt64Field("play_keys");
snapshot.bugReports = result.getInt64Field("bug_reports");
snapshot.unresolvedBugReports = result.getInt64Field("unresolved_bug_reports");
snapshot.petNames = result.getInt64Field("pet_names");
snapshot.pendingPetNames = result.getInt64Field("pending_pet_names");
snapshot.activityLogMaxId = result.getInt64Field("activity_log_max_id");
snapshot.chatLogMaxId = result.getInt64Field("chat_log_max_id");
snapshot.commandLogMaxId = result.getInt64Field("command_log_max_id");
snapshot.auditLogMaxId = result.getInt64Field("audit_log_max_id");
snapshot.mailMaxId = result.getInt64Field("mail_max_id");
snapshot.openEconomyFlags = result.getInt64Field("open_economy_flags");
snapshot.economyFlagsMaxId = result.getInt64Field("economy_flags_max_id");
return snapshot;
}
std::optional<LWOOBJID> SQLiteDatabase::GetModelPropertyId(const LWOOBJID modelID) {
auto [_, result] = ExecuteSelect("SELECT property_id FROM properties_contents WHERE id = ? LIMIT 1;", modelID);
if (result.eof()) return std::nullopt;
return result.getInt64Field("property_id");
}
std::optional<int32_t> SQLiteDatabase::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.eof()) return std::nullopt;
return result.getIntField("id");
}
void SQLiteDatabase::SetAccountPlayKey(const uint32_t accountId, const int32_t playkeyId) {
ExecuteUpdate("UPDATE accounts SET play_key_id = ? WHERE id = ?;", playkeyId, accountId);
}
std::optional<IAccountEmails::EmailInfo> SQLiteDatabase::GetAccountEmail(const uint32_t accountId) {
auto [_, result] = ExecuteSelect("SELECT email, email_confirmed_at FROM accounts WHERE id = ? LIMIT 1;", accountId);
if (result.eof()) return std::nullopt;
return IAccountEmails::EmailInfo{ result.fieldIsNull("email") ? "" : result.getStringField("email"), !result.fieldIsNull("email_confirmed_at") };
}
void SQLiteDatabase::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> SQLiteDatabase::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.eof()) return std::nullopt;
return static_cast<uint32_t>(result.getIntField("id"));
}
void SQLiteDatabase::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> SQLiteDatabase::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.eof()) token = IAccountEmails::AccountToken{ static_cast<uint32_t>(result.getIntField("account_id")), result.getStringField("data") };
}
ExecuteDelete("DELETE FROM account_tokens WHERE token_hash = ?;", tokenHash);
return token;
}
void SQLiteDatabase::DeleteAccountTokens(const uint32_t accountId, const std::string_view purpose) {
ExecuteDelete("DELETE FROM account_tokens WHERE account_id = ? AND purpose = ?;", accountId, purpose);
}
void SQLiteDatabase::DeleteExpiredAccountTokens() {
ExecuteDelete("DELETE FROM account_tokens WHERE expires_at <= ?;", static_cast<int64_t>(std::time(nullptr)));
}
int64_t SQLiteDatabase::GetSessionsValidAfter(const uint32_t accountId) {
auto [_, result] = ExecuteSelect("SELECT sessions_valid_after FROM accounts WHERE id = ? LIMIT 1;", accountId);
return result.eof() ? 0 : result.getInt64Field("sessions_valid_after");
}
void SQLiteDatabase::SetSessionsValidAfter(const uint32_t accountId, const int64_t time) {
ExecuteUpdate("UPDATE accounts SET sessions_valid_after = ? WHERE id = ?;", time, accountId);
}
uint32_t SQLiteDatabase::ApprovePreviouslyApprovedPetNames() {
// The derived table keeps this valid on MySQL, which 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>> SQLiteDatabase::GetAllPetNames() {
std::vector<std::pair<LWOOBJID, std::string>> pets;
auto [_, result] = ExecuteSelect("SELECT id, pet_name FROM pet_names;");
while (!result.eof()) {
pets.emplace_back(result.getInt64Field("id"), result.getStringField("pet_name"));
result.nextRow();
}
return pets;
}
void SQLiteDatabase::ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) {
auto [_, result] = ExecuteSelect("SELECT id, xml_data FROM charxml;");
while (!result.eof()) {
visit(result.getInt64Field("id"), result.getStringField("xml_data"));
result.nextRow();
}
}
void SQLiteDatabase::ForEachCharacterXmlContaining(const std::string& needle, const std::function<void(LWOOBJID, const std::string&)>& visit) {
auto [_, result] = ExecuteSelect("SELECT id, xml_data FROM charxml WHERE instr(xml_data, ?) > 0;", needle);
while (!result.eof()) {
visit(result.getInt64Field("id"), result.getStringField("xml_data"));
result.nextRow();
}
}
uint32_t SQLiteDatabase::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));"));
}

View File

@@ -0,0 +1,299 @@
#include "SQLiteDatabase.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 SQLiteDatabase::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 CONFLICT(day, character_id, source) DO UPDATE SET gained = gained + excluded.gained, spent = spent + excluded.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 CONFLICT(day, character_id, source) DO UPDATE SET gained = gained + excluded.gained, lost = lost + excluded.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 CONFLICT(day, lot, source, gm) DO UPDATE SET created = created + excluded.created, destroyed = destroyed + excluded.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 CONFLICT(zone, kind, day, clone_id, lot, cell_x, cell_z) DO UPDATE SET events = events + excluded.events, quantity = quantity + excluded.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 CONFLICT(day, zone, clone_id, stat, gm) DO UPDATE SET amount = amount + excluded.amount;",
stat.day, stat.zone, stat.clone, stat.stat, stat.gm ? 1 : 0, stat.amount);
}
transaction.Commit();
}
nlohmann::json SQLiteDatabase::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.eof()) {
rows.push_back({ {"day", result.getIntField("day")}, {"source", result.getIntField("source")},
{"gained", result.getInt64Field("gained")}, {"spent", result.getInt64Field("spent")} });
result.nextRow();
}
return rows;
}
nlohmann::json SQLiteDatabase::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.eof()) {
rows.push_back({ {"day", result.getIntField("day")}, {"source", result.getIntField("source")},
{"gained", result.getInt64Field("gained")}, {"lost", result.getInt64Field("lost")} });
result.nextRow();
}
return rows;
}
nlohmann::json SQLiteDatabase::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.eof()) {
rows.push_back({ {"day", result.getIntField("day")}, {"source", result.getIntField("source")},
{"created", result.getInt64Field("created")}, {"destroyed", result.getInt64Field("destroyed")} });
result.nextRow();
}
return rows;
}
nlohmann::json SQLiteDatabase::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.eof()) {
rows.push_back({ {"character_id", std::to_string(result.getInt64Field("character_id"))}, {"name", result.getStringField("name")},
{"gained", result.getInt64Field("gained")}, {"spent", result.getInt64Field("spent")} });
result.nextRow();
}
return rows;
}
nlohmann::json SQLiteDatabase::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.eof()) {
rows.push_back({ {"lot", result.getIntField("lot")}, {"created", result.getInt64Field("created")}, {"destroyed", result.getInt64Field("destroyed")} });
result.nextRow();
}
return rows;
}
namespace {
nlohmann::json TransferRow(CppSQLite3Query& result) {
return {
{"id", result.getInt64Field("id")},
{"time", result.getInt64Field("time")},
{"method", result.getIntField("method")},
{"item_id", std::to_string(result.getInt64Field("item_id"))},
{"new_item_id", std::to_string(result.getInt64Field("new_item_id"))},
{"lot", result.getIntField("lot")},
{"count", result.getIntField("count")},
{"coins", result.getInt64Field("coins")},
{"from_character", std::to_string(result.getInt64Field("from_character"))},
{"from_name", result.fieldIsNull("from_name") ? "" : result.getStringField("from_name")},
{"to_character", std::to_string(result.getInt64Field("to_character"))},
{"to_name", result.fieldIsNull("to_name") ? "" : result.getStringField("to_name")},
{"zone", result.getIntField("zone")},
{"merged", result.fieldIsNull("merged") ? nlohmann::json() : nlohmann::json(result.getIntField("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 SQLiteDatabase::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 ? OFFSET ?;";
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.eof() ? 0 : countResult.getIntField("count");
auto [__, result] = run(query, length, start);
nlohmann::json data = nlohmann::json::array();
while (!result.eof()) {
data.push_back(TransferRow(result));
result.nextRow();
}
return { {"draw", 0}, {"recordsTotal", total}, {"recordsFiltered", total}, {"data", data} };
}
nlohmann::json SQLiteDatabase::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.eof()) {
rows.push_back(TransferRow(result));
result.nextRow();
}
return rows;
}
void SQLiteDatabase::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.eof()) {
visit({ static_cast<uint64_t>(result.getInt64Field("id")), result.getInt64Field("receiver_id"), result.getInt64Field("attachment_id"),
result.getIntField("attachment_lot"), static_cast<uint32_t>(result.getIntField("attachment_count")) });
result.nextRow();
}
}
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 SQLiteDatabase::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();
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"zone", result.getIntField("zone")}, {"clone", static_cast<uint32_t>(result.getInt64Field("clone_id"))}, {"events", result.getInt64Field("events")}, {"quantity", result.getInt64Field("quantity")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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();
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"lot", result.getIntField("lot")}, {"events", result.getInt64Field("events")}, {"quantity", result.getInt64Field("quantity")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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();
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"x", result.getIntField("cell_x")}, {"z", result.getIntField("cell_z")}, {"events", result.getInt64Field("events")}, {"quantity", result.getInt64Field("quantity")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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();
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"day", result.getIntField("day")}, {"kind", result.getIntField("kind")}, {"events", result.getInt64Field("events")}, {"quantity", result.getInt64Field("quantity")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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();
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"day", result.getIntField("day")}, {"zone", result.getIntField("zone")}, {"clone", static_cast<uint32_t>(result.getInt64Field("clone_id"))},
{"lot", result.getIntField("lot")}, {"events", result.getInt64Field("events")}, {"quantity", result.getInt64Field("quantity")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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();
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"day", result.getIntField("day")}, {"stat", result.getIntField("stat")}, {"amount", result.getInt64Field("amount")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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();
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"zone", result.getIntField("zone")}, {"clone", static_cast<uint32_t>(result.getInt64Field("clone_id"))}, {"stat", result.getIntField("stat")}, {"amount", result.getInt64Field("amount")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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;");
for (; !result.eof(); result.nextRow()) {
owners.push_back({ {"clone", static_cast<uint32_t>(result.getInt64Field("prop_clone_id"))}, {"character_id", std::to_string(result.getInt64Field("id"))},
{"name", std::string(result.getStringField("name"))} });
}
}
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;");
for (; !props.eof(); props.nextRow()) {
properties.push_back({ {"clone", static_cast<uint32_t>(props.getInt64Field("clone_id"))}, {"zone", props.getIntField("zone_id")},
{"id", std::to_string(props.getInt64Field("id"))}, {"name", std::string(props.getStringField("name"))}, {"owner_id", std::to_string(props.getInt64Field("owner_id"))} });
}
}
return { {"owners", owners}, {"properties", properties} };
}

View File

@@ -0,0 +1,37 @@
#include "SQLiteDatabase.h"
std::vector<IFeaturedProperties::FeaturedSlot> SQLiteDatabase::GetFeaturedPropertySlots() {
std::vector<FeaturedSlot> slots;
auto [_, result] = ExecuteSelect("SELECT * FROM featured_properties ORDER BY template_id;");
for (; !result.eof(); result.nextRow()) {
slots.push_back({ static_cast<uint32_t>(result.getIntField("template_id")), static_cast<uint8_t>(result.getIntField("mode")),
result.getInt64Field("property_id"), result.getInt64Field("updated_at"), result.getStringField("updated_by"),
static_cast<uint32_t>(result.getInt64Field("zone_id")) });
}
return slots;
}
void SQLiteDatabase::SetFeaturedPropertySlot(const FeaturedSlot& slot) {
ExecuteInsert(
"INSERT INTO featured_properties (template_id, mode, property_id, updated_at, updated_by, zone_id) VALUES (?, ?, ?, ?, ?, ?) "
"ON CONFLICT(template_id) DO UPDATE SET mode = excluded.mode, property_id = excluded.property_id, updated_at = excluded.updated_at, updated_by = excluded.updated_by, zone_id = excluded.zone_id;",
slot.templateId, static_cast<uint32_t>(slot.mode), slot.propertyId, slot.updatedAt, slot.updatedBy, slot.zoneId);
}
IFeaturedProperties::FeaturedSettings SQLiteDatabase::GetFeaturedPropertiesSettings() {
FeaturedSettings settings;
auto [_, result] = ExecuteSelect("SELECT * FROM featured_properties_settings WHERE id = 1;");
if (!result.eof()) {
settings.fullAuto = result.getIntField("full_auto") != 0;
settings.updatedAt = result.getInt64Field("updated_at");
settings.updatedBy = result.getStringField("updated_by");
}
return settings;
}
void SQLiteDatabase::SetFeaturedPropertiesSettings(const FeaturedSettings& settings) {
ExecuteInsert(
"INSERT INTO featured_properties_settings (id, full_auto, updated_at, updated_by) VALUES (1, ?, ?, ?) "
"ON CONFLICT(id) DO UPDATE SET full_auto = excluded.full_auto, updated_at = excluded.updated_at, updated_by = excluded.updated_by;",
settings.fullAuto ? 1 : 0, settings.updatedAt, settings.updatedBy);
}

View File

@@ -89,3 +89,18 @@ void SQLiteDatabase::IncrementNumWins(const LWOOBJID playerId, const uint32_t ga
void SQLiteDatabase::IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) {
ExecuteUpdate("UPDATE leaderboard SET timesPlayed = timesPlayed + 1, last_played = CURRENT_TIMESTAMP WHERE character_id = ? AND game_id = ?;", playerId, gameId);
}
std::map<uint32_t, uint32_t> SQLiteDatabase::GetLeaderboardSizes() {
std::map<uint32_t, uint32_t> sizes;
auto [_, result] = ExecuteSelect("SELECT game_id, COUNT(*) AS scores FROM leaderboard GROUP BY game_id;");
for (; !result.eof(); result.nextRow()) sizes[result.getIntField("game_id")] = result.getIntField("scores");
return sizes;
}
void SQLiteDatabase::DeleteLeaderboardScore(const LWOOBJID playerId, const uint32_t gameId) {
ExecuteDelete("DELETE FROM leaderboard WHERE character_id = ? AND game_id = ?;", playerId, gameId);
}
void SQLiteDatabase::ResetLeaderboard(const uint32_t gameId) {
ExecuteDelete("DELETE FROM leaderboard WHERE game_id = ?;", gameId);
}

View File

@@ -0,0 +1,234 @@
#include "SQLiteDatabase.h"
#include "GeneralUtils.h"
namespace {
uint64_t U64(CppSQLite3Query& result, const char* field) { return static_cast<uint64_t>(result.getInt64Field(field)); }
uint32_t U32(CppSQLite3Query& result, const char* field) { return static_cast<uint32_t>(result.getInt64Field(field)); }
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(CppSQLite3Query& result, const char* field) {
return result.fieldIsNull(field) ? "" : std::string(result.getStringField(field));
}
ILiveOps::LiveEvent ReadEvent(CppSQLite3Query& result) {
ILiveOps::LiveEvent row;
row.id = U64(result, "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.getIntField("instance_id");
row.config = Text(result, "config");
row.startsAt = result.getInt64Field("starts_at");
row.endsAt = result.getInt64Field("ends_at");
row.state = static_cast<ILiveOps::eLiveEventState>(result.getIntField("state"));
row.endedAt = result.getInt64Field("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(CppSQLite3Query& result) {
ILiveOps::Challenge row;
row.id = U64(result, "id");
row.title = Text(result, "title");
row.description = Text(result, "description");
row.metricKind = static_cast<ILiveOps::eChallengeMetric>(result.getIntField("metric_kind"));
row.metric = U32(result, "metric");
row.lot = result.getIntField("lot");
row.zones = SplitZones(Text(result, "zones"));
row.target = result.getInt64Field("target");
row.startsAt = result.getInt64Field("starts_at");
row.endsAt = result.getInt64Field("ends_at");
row.includeStaff = result.getIntField("include_staff") != 0;
row.isPublic = result.getIntField("is_public") != 0;
row.rewardCoins = result.getInt64Field("reward_coins");
row.rewardItems = Text(result, "reward_items");
row.rewardMin = result.getInt64Field("reward_min");
row.state = static_cast<ILiveOps::eChallengeState>(result.getIntField("state"));
row.milestone = static_cast<uint8_t>(U32(result, "milestone"));
row.completedAt = result.getInt64Field("completed_at");
row.rewardedAt = result.getInt64Field("rewarded_at");
row.rewardedCount = U32(result, "rewarded_count");
row.endAnnouncedAt = result.getInt64Field("end_announced_at");
row.createdAt = result.getInt64Field("created_at");
row.createdBy = Text(result, "created_by");
row.updatedAt = result.getInt64Field("updated_at");
row.updatedBy = Text(result, "updated_by");
return row;
}
ILiveOps::Reward ReadReward(CppSQLite3Query& result) {
return { U64(result, "challenge_id"), result.getInt64Field("character_id"), result.getInt64Field("amount"), result.getInt64Field("coins"),
result.getInt64Field("rewarded_at"), result.getInt64Field("claimed_at") };
}
std::vector<ILiveOps::LiveOpsScore> ReadScores(CppSQLite3Query& result, const char* idField) {
std::vector<ILiveOps::LiveOpsScore> scores;
for (; !result.eof(); result.nextRow()) {
scores.push_back({ U64(result, idField), result.getInt64Field("character_id"), Text(result, "name"), result.getInt64Field("amount") });
}
return scores;
}
}
std::vector<ILiveOps::LiveEvent> SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) rows.push_back(ReadEvent(result));
return rows;
}
std::optional<ILiveOps::LiveEvent> SQLiteDatabase::GetLiveEvent(uint64_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM live_events WHERE id = ?;", id);
if (result.eof()) return std::nullopt;
return ReadEvent(result);
}
uint64_t SQLiteDatabase::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_rowid() AS id;");
return result.eof() ? 0 : U64(result, "id");
}
bool SQLiteDatabase::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> SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) {
rows.push_back({ U64(result, "event_id"), U32(result, "zone_id"), U32(result, "instance_id"), Text(result, "status"), result.getInt64Field("updated_at") });
}
}
return rows;
}
void SQLiteDatabase::SetLiveEventInstance(const LiveEventInstance& row) {
ExecuteInsert("INSERT INTO live_event_instances (event_id, zone_id, instance_id, status, updated_at) VALUES (?, ?, ?, ?, ?) "
"ON CONFLICT(event_id, zone_id, instance_id) DO UPDATE SET status = excluded.status, updated_at = excluded.updated_at;",
row.eventId, row.zoneId, row.instanceId, row.status, row.updatedAt);
}
void SQLiteDatabase::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 CONFLICT(event_id, character_id) DO UPDATE SET amount = amount + excluded.amount, updated_at = excluded.updated_at;",
eventId, characterId, amount, time);
}
}
std::vector<ILiveOps::LiveOpsScore> SQLiteDatabase::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> SQLiteDatabase::GetChallenges(bool openOnly) {
std::vector<Challenge> rows;
auto [_, result] = ExecuteSelect(std::string("SELECT * FROM challenges") + (openOnly ? " WHERE state = 0" : "") + " ORDER BY id DESC;");
for (; !result.eof(); result.nextRow()) rows.push_back(ReadChallenge(result));
return rows;
}
std::optional<ILiveOps::Challenge> SQLiteDatabase::GetChallenge(uint64_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM challenges WHERE id = ?;", id);
if (result.eof()) return std::nullopt;
return ReadChallenge(result);
}
uint64_t SQLiteDatabase::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_rowid() AS id;");
return result.eof() ? 0 : U64(result, "id");
}
void SQLiteDatabase::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 SQLiteDatabase::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 SQLiteDatabase::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 CONFLICT(challenge_id, character_id) DO UPDATE SET amount = amount + excluded.amount, updated_at = excluded.updated_at;",
row.challengeId, row.characterId, row.amount, time);
}
}
std::map<uint64_t, ILiveOps::ChallengeTotal> SQLiteDatabase::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.eof()) totals[id] = { result.getInt64Field("total"), U32(result, "contributors") };
}
return totals;
}
std::vector<ILiveOps::LiveOpsScore> SQLiteDatabase::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 ? int64_t{ -1 } : static_cast<int64_t>(limit));
return ReadScores(result, "challenge_id");
}
std::map<uint64_t, int64_t> SQLiteDatabase::GetCharacterContributions(LWOOBJID characterId) {
std::map<uint64_t, int64_t> amounts;
auto [_, result] = ExecuteSelect("SELECT challenge_id, amount FROM challenge_contributions WHERE character_id = ?;", characterId);
for (; !result.eof(); result.nextRow()) amounts[U64(result, "challenge_id")] = result.getInt64Field("amount");
return amounts;
}
std::vector<ILiveOps::Reward> SQLiteDatabase::GetChallengeRewards(uint64_t challengeId) {
std::vector<Reward> rows;
auto [_, result] = ExecuteSelect("SELECT * FROM challenge_rewards WHERE challenge_id = ?;", challengeId);
for (; !result.eof(); result.nextRow()) rows.push_back(ReadReward(result));
return rows;
}
void SQLiteDatabase::InsertChallengeReward(const Reward& row) {
ExecuteInsert("INSERT OR 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> SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) rows.push_back(ReadReward(result));
return rows;
}
bool SQLiteDatabase::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;
}

View File

@@ -3,8 +3,8 @@
void SQLiteDatabase::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,
@@ -14,8 +14,9 @@ void SQLiteDatabase::InsertNewMail(const MailInfo& mail) {
mail.body,
mail.itemID,
mail.itemLOT,
0,
mail.itemCount);
mail.itemSubkey,
mail.itemCount,
mail.itemConfig);
}
std::vector<MailInfo> SQLiteDatabase::GetMailForPlayer(const LWOOBJID characterId, const uint32_t numberOfMail) {
@@ -32,9 +33,9 @@ std::vector<MailInfo> SQLiteDatabase::GetMailForPlayer(const LWOOBJID characterI
mail.subject = res.getStringField("subject");
mail.body = res.getStringField("body");
mail.senderUsername = res.getStringField("sender_name");
mail.itemID = res.getIntField("attachment_id");
mail.itemID = res.getInt64Field("attachment_id");
mail.itemLOT = res.getIntField("attachment_lot");
mail.itemSubkey = res.getIntField("attachment_subkey");
mail.itemSubkey = res.getInt64Field("attachment_subkey");
mail.itemCount = res.getIntField("attachment_count");
mail.timeSent = res.getInt64Field("time_sent");
mail.wasRead = res.getIntField("was_read");
@@ -47,15 +48,20 @@ std::vector<MailInfo> SQLiteDatabase::GetMailForPlayer(const LWOOBJID characterI
}
std::optional<MailInfo> SQLiteDatabase::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.eof()) {
return std::nullopt;
}
MailInfo toReturn;
toReturn.id = mailId;
toReturn.senderId = res.getInt64Field("sender_id");
toReturn.itemID = res.getInt64Field("attachment_id");
toReturn.itemLOT = res.getIntField("attachment_lot");
toReturn.itemSubkey = res.getInt64Field("attachment_subkey");
toReturn.itemCount = res.getIntField("attachment_count");
toReturn.itemConfig = res.fieldIsNull("attachment_config") ? "" : res.getStringField("attachment_config");
toReturn.receiverId = res.getInt64Field("receiver_id");
return toReturn;

View File

@@ -0,0 +1,140 @@
#include "SQLiteDatabase.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) {
// MySQL compares names without case; so does this
case eOrder::CHARACTER: column = "character_name COLLATE NOCASE"; break;
case eOrder::STARTED_BY: column = "started_by COLLATE NOCASE"; 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(CppSQLite3Query& r) {
IMessageCaptures::MessageCaptureSession s;
s.id = static_cast<uint64_t>(r.getInt64Field("id"));
s.characterId = r.getInt64Field("character_id");
s.characterName = r.getStringField("character_name");
s.accountId = static_cast<uint32_t>(r.getInt64Field("account_id"));
s.accountName = r.getStringField("account_name");
s.startedById = static_cast<uint32_t>(r.getInt64Field("started_by_id"));
s.startedBy = r.getStringField("started_by");
s.startedAt = r.getInt64Field("started_at");
s.endsAt = r.getInt64Field("ends_at");
s.endedAt = r.getInt64Field("ended_at");
s.endReason = r.getStringField("end_reason");
s.toServer = r.getIntField("to_server") != 0;
s.toClient = r.getIntField("to_client") != 0;
s.onlyMessages = r.getStringField("only_messages");
s.skipMessages = r.getStringField("skip_messages");
s.zoneId = static_cast<uint32_t>(r.getInt64Field("zone_id"));
s.instanceId = static_cast<uint32_t>(r.getInt64Field("instance_id"));
s.cloneId = static_cast<uint32_t>(r.getInt64Field("clone_id"));
s.zones = r.getStringField("zones");
s.messageCount = static_cast<uint64_t>(r.getInt64Field("message_count"));
s.byteCount = static_cast<uint64_t>(r.getInt64Field("byte_count"));
s.dropped = static_cast<uint64_t>(r.getInt64Field("dropped"));
return s;
}
}
uint64_t SQLiteDatabase::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_rowid() AS id;"); // this connection's insert
return last.eof() ? 0 : static_cast<uint64_t>(last.getInt64Field("id"));
}
void SQLiteDatabase::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 SQLiteDatabase::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> SQLiteDatabase::GetMessageCaptureSession(uint64_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM message_capture_sessions WHERE id = ?;", id);
if (result.eof()) return std::nullopt;
return Session(result);
}
std::vector<IMessageCaptures::MessageCaptureSession> SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) sessions.push_back(Session(result));
return sessions;
}
uint64_t SQLiteDatabase::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.eof() ? 0 : static_cast<uint64_t>(result.getInt64Field("count"));
}
std::vector<IMessageCaptures::MessageCaptureRecord> SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) {
MessageCaptureRecord e;
e.sessionId = static_cast<uint64_t>(result.getInt64Field("session_id"));
e.seq = static_cast<uint32_t>(result.getInt64Field("seq"));
e.timeMs = result.getInt64Field("time_ms");
e.direction = static_cast<uint8_t>(result.getIntField("direction"));
e.messageId = static_cast<uint16_t>(result.getIntField("message_id"));
e.objectId = result.getInt64Field("object_id");
e.bits = static_cast<uint32_t>(result.getInt64Field("bits"));
e.droppedBefore = static_cast<uint32_t>(result.getInt64Field("dropped_before"));
e.zoneId = static_cast<uint32_t>(result.getInt64Field("zone_id"));
e.instanceId = static_cast<uint32_t>(result.getInt64Field("instance_id"));
e.cloneId = static_cast<uint32_t>(result.getInt64Field("clone_id"));
int length = 0;
const auto* blob = result.getBlobField("payload", length);
if (blob && length > 0) e.payload.assign(reinterpret_cast<const char*>(blob), length);
e.decoded = result.getStringField("decoded");
entries.push_back(std::move(e));
}
return entries;
}
void SQLiteDatabase::DeleteMessageCaptureSession(uint64_t id) {
ExecuteDelete("DELETE FROM message_capture_entries WHERE session_id = ?;", id);
ExecuteDelete("DELETE FROM message_capture_sessions WHERE id = ?;", id);
}

View File

@@ -0,0 +1,116 @@
#include "SQLiteDatabase.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 = ?)";
IModeration::PlayerReport ReadReport(CppSQLite3Query& r) {
return { static_cast<uint64_t>(r.getInt64Field("id")), r.getInt64Field("created_at"), r.getStringField("kind"), r.getInt64Field("reporter_id"),
static_cast<uint32_t>(r.getIntField("reporter_account_id")), r.getInt64Field("object_id"), r.getIntField("object_lot"),
r.getInt64Field("target_character_id"), static_cast<uint32_t>(r.getIntField("target_account_id")), r.getInt64Field("property_id"),
static_cast<uint32_t>(r.getIntField("zone_id")), static_cast<uint32_t>(r.getIntField("instance_id")), static_cast<uint32_t>(r.getIntField("clone_id")),
r.getStringField("body"), static_cast<uint8_t>(r.getIntField("status")), r.getStringField("handled_by"), r.getInt64Field("handled_at"),
r.getStringField("resolution") };
}
IModeration::LinkedAccount ReadLinked(CppSQLite3Query& r, eAccountLink link) {
return { static_cast<uint32_t>(r.getIntField("id")), r.getStringField("name"), static_cast<uint8_t>(r.getIntField("gm_level")),
r.getIntField("banned") != 0, static_cast<uint8_t>(link), static_cast<uint32_t>(r.getIntField("shared")), r.getInt64Field("last_seen") };
}
}
void SQLiteDatabase::SetStrikeStep(uint64_t strikeId, const std::string& step, uint32_t count) {
ExecuteUpdate("UPDATE account_strikes SET step = ?, step_count = ? WHERE id = ?;", step, count, static_cast<int64_t>(strikeId));
}
std::vector<IModeration::AppliedStrikeStep> SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) steps.push_back({ result.getStringField("step"), static_cast<uint32_t>(result.getIntField("step_count")), result.getInt64Field("created_at") });
return steps;
}
uint64_t SQLiteDatabase::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) 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_rowid() AS id;");
return last.eof() ? 0 : static_cast<uint64_t>(last.getInt64Field("id"));
}
std::vector<IModeration::PlayerReport> SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) reports.push_back(ReadReport(result));
return reports;
}
uint32_t SQLiteDatabase::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.eof() ? 0 : static_cast<uint32_t>(result.getIntField("count"));
}
std::optional<IModeration::PlayerReport> SQLiteDatabase::GetPlayerReport(uint64_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM player_reports WHERE id = ?;", static_cast<int64_t>(id));
if (result.eof()) return std::nullopt;
return ReadReport(result);
}
void SQLiteDatabase::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, static_cast<int64_t>(id));
}
std::vector<IModeration::ChatFilterWord> SQLiteDatabase::GetChatFilterWords() {
std::vector<ChatFilterWord> words;
auto [_, result] = ExecuteSelect("SELECT * FROM chat_filter_words ORDER BY word;");
for (; !result.eof(); result.nextRow()) {
words.push_back({ result.getStringField("word"), result.getIntField("allowed") != 0, result.getStringField("added_by"), result.getInt64Field("added_at") });
}
return words;
}
void SQLiteDatabase::SetChatFilterWord(const ChatFilterWord& w) {
ExecuteInsert("INSERT INTO chat_filter_words (word, allowed, added_by, added_at) VALUES (?, ?, ?, ?) "
"ON CONFLICT(word) DO UPDATE SET allowed = excluded.allowed, added_by = excluded.added_by, added_at = excluded.added_at;", w.word, w.allowed, w.addedBy, w.addedAt);
}
bool SQLiteDatabase::DeleteChatFilterWord(const std::string& word) {
return ExecuteUpdate("DELETE FROM chat_filter_words WHERE word = ?;", word) > 0;
}
void SQLiteDatabase::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 CONFLICT(account_id, address) DO UPDATE SET last_seen = excluded.last_seen, logins = logins + 1;", accountId, address, time, time);
}
std::vector<IModeration::LinkedAccount> SQLiteDatabase::GetLinkedAccounts(uint32_t accountId) {
std::vector<LinkedAccount> links;
{
auto [_, result] = 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);
for (; !result.eof(); result.nextRow()) links.push_back(ReadLinked(result, eAccountLink::PLAY_KEY));
}
{
auto [_, result] = 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);
for (; !result.eof(); result.nextRow()) links.push_back(ReadLinked(result, eAccountLink::EMAIL));
}
{
auto [_, result] = 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);
for (; !result.eof(); result.nextRow()) links.push_back(ReadLinked(result, eAccountLink::LOGIN_ADDRESS));
}
return links;
}
uint32_t SQLiteDatabase::CountLoginAddresses(uint32_t accountId) {
auto [_, result] = ExecuteSelect("SELECT COUNT(*) AS count FROM account_login_addresses WHERE account_id = ?;", accountId);
return result.eof() ? 0 : static_cast<uint32_t>(result.getIntField("count"));
}

View File

@@ -1,18 +1,23 @@
#include "SQLiteDatabase.h"
#include "GeneralUtils.h"
#include "json.hpp"
void SQLiteDatabase::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 CONFLICT(id) DO UPDATE SET pet_name = ?, approved = ?;",
"INSERT INTO `pet_names` (`id`, `pet_name`, `approved`, `owner_id`) VALUES (?, ?, ?, ?) "
"ON CONFLICT(id) DO UPDATE SET 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> SQLiteDatabase::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.eof()) {
return std::nullopt;
@@ -21,6 +26,76 @@ std::optional<IPetNames::Info> SQLiteDatabase::GetPetNameInfo(const LWOOBJID& pe
IPetNames::Info toReturn;
toReturn.petName = result.getStringField("pet_name");
toReturn.approvalStatus = result.getIntField("approved");
toReturn.ownerId = result.fieldIsNull("owner_id") ? 0 : result.getInt64Field("owner_id");
return toReturn;
}
std::string SQLiteDatabase::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 '%' || ? || '%' OR p.id = ? OR c.name LIKE '%' || ? || '%')";
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 ? OFFSET ?;";
auto [__, totalCountResult] = ExecuteSelect("SELECT COUNT(*) as count FROM pet_names p" + (pendingOnly ? " WHERE " + pendingFilter : std::string{}) + ";");
uint32_t totalRecords = totalCountResult.eof() ? 0 : totalCountResult.getIntField("count");
uint32_t filteredRecords = totalRecords;
if (!search.empty()) {
auto [___, filteredCountResult] = ExecuteSelect("SELECT COUNT(*) as count" + from + whereClause + ";", search, searchId, search);
filteredRecords = filteredCountResult.eof() ? 0 : filteredCountResult.getIntField("count");
}
auto [stmt, result] = !search.empty()
? ExecuteSelect(mainQuery, search, searchId, search, length, start)
: ExecuteSelect(mainQuery, length, start);
nlohmann::json dataArray = nlohmann::json::array();
while (!result.eof()) {
const bool hasOwner = !result.fieldIsNull("owner_id") && result.getInt64Field("owner_id") != 0;
dataArray.push_back({
{"id", std::to_string(result.getInt64Field("id"))},
{"pet_name", result.getStringField("pet_name")},
{"approved", result.getIntField("approved")},
{"owner_id", hasOwner ? std::to_string(result.getInt64Field("owner_id")) : ""},
{"owner_name", std::string(result.fieldIsNull("owner_name") ? "" : result.getStringField("owner_name"))}
});
result.nextRow();
}
return nlohmann::json({{"draw", 0}, {"recordsTotal", totalRecords}, {"recordsFiltered", filteredRecords}, {"data", dataArray}}).dump();
}
void SQLiteDatabase::ApprovePetName(const int64_t id) {
ExecuteUpdate("UPDATE pet_names SET approved = 2 WHERE id = ?;", id);
}
void SQLiteDatabase::RejectPetName(const int64_t id) {
ExecuteDelete("DELETE FROM pet_names WHERE id = ?;", id);
}
std::vector<LWOOBJID> SQLiteDatabase::GetPetsWithUnknownOwner() {
std::vector<LWOOBJID> pets;
auto [_, result] = ExecuteSelect("SELECT id FROM pet_names WHERE owner_id IS NULL;");
for (; !result.eof(); result.nextRow()) pets.push_back(result.getInt64Field("id"));
return pets;
}
void SQLiteDatabase::SetPetOwner(const LWOOBJID petId, const LWOOBJID ownerId) {
ExecuteUpdate("UPDATE pet_names SET owner_id = ? WHERE id = ?;", ownerId, petId);
}

View File

@@ -9,3 +9,20 @@ std::optional<bool> SQLiteDatabase::IsPlaykeyActive(const int32_t playkeyId) {
return keyCheckRes.getIntField("active");
}
#include "json.hpp"
void SQLiteDatabase::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, datetime('now'), ?);", keyString, uses, notes);
}
void SQLiteDatabase::SetPlayKeyActive(const int32_t playkeyId, const bool active) {
ExecuteUpdate("UPDATE play_keys SET active = ? WHERE id = ?;", active ? 1 : 0, playkeyId);
}
uint32_t SQLiteDatabase::GetPlayKeyCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM play_keys;");
if (res.eof()) return 0;
return res.getIntField("count");
}

View File

@@ -0,0 +1,72 @@
#include "SQLiteDatabase.h"
void SQLiteDatabase::InsertPositionSamples(const std::vector<PositionSample>& samples) {
if (samples.empty()) return;
const auto prevCommit = GetAutoCommit();
SetAutoCommit(false);
for (const auto& sample : samples) {
ExecuteInsert("INSERT OR 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> SQLiteDatabase::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, 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;
for (; !result.eof(); result.nextRow()) {
samples.push_back({ result.getInt64Field("t"), result.getInt64Field("character_id"), zoneId, static_cast<uint32_t>(result.getIntField("instance_id")),
static_cast<uint32_t>(result.getIntField("clone_id")), static_cast<float>(result.getFloatField("x")), static_cast<float>(result.getFloatField("y")),
static_cast<float>(result.getFloatField("z")) });
}
return samples;
}
std::vector<IPlayerPositions::PositionInstance> SQLiteDatabase::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;
for (; !result.eof(); result.nextRow()) {
instances.push_back({ static_cast<uint32_t>(result.getIntField("zone_id")), static_cast<uint32_t>(result.getIntField("instance_id")),
static_cast<uint32_t>(result.getIntField("clone_id")), result.getInt64Field("first"), result.getInt64Field("last"),
static_cast<uint32_t>(result.getIntField("players")) });
}
return instances;
}
uint32_t SQLiteDatabase::PrunePositionSamples(int64_t beforeTime) {
return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM player_positions WHERE time < ?;", beforeTime));
}
nlohmann::json SQLiteDatabase::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();
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"day", result.getIntField("day")}, {"x", result.getIntField("cell_x")}, {"z", result.getIntField("cell_z")}, {"events", result.getInt64Field("events")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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();
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"character_id", std::to_string(result.getInt64Field("character_id"))}, {"name", std::string(result.getStringField("name"))},
{"first", result.getInt64Field("first")}, {"last", result.getInt64Field("last")} });
}
return rows;
}

View File

@@ -1,4 +1,5 @@
#include "SQLiteDatabase.h"
#include "GeneralUtils.h"
#include "ePropertySortType.h"
IProperty::Info ReadPropertyInfo(CppSQLite3Query& propertyEntry) {
@@ -15,6 +16,7 @@ IProperty::Info ReadPropertyInfo(CppSQLite3Query& propertyEntry) {
toReturn.reputation = propertyEntry.getIntField("reputation");
toReturn.modApproved = propertyEntry.getIntField("mod_approved");
toReturn.performanceCost = propertyEntry.getFloatField("performance_cost");
toReturn.zoneId = static_cast<uint32_t>(propertyEntry.getIntField("zone_id"));
return toReturn;
}
@@ -141,7 +143,7 @@ IProperty::PropertyEntranceResult SQLiteDatabase::GetProperties(const IProperty:
std::optional<IProperty::Info> SQLiteDatabase::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.eof()) {
@@ -188,7 +190,7 @@ void SQLiteDatabase::InsertNewProperty(const IProperty::Info& info, const uint32
std::optional<IProperty::Info> SQLiteDatabase::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.eof()) {
@@ -197,3 +199,123 @@ std::optional<IProperty::Info> SQLiteDatabase::GetPropertyInfo(const LWOOBJID id
return ReadPropertyInfo(propertyEntry);
}
#include "json.hpp"
uint32_t SQLiteDatabase::GetPropertyCount() {
auto [_, res] = ExecuteSelect("SELECT COUNT(*) as count FROM properties;");
if (res.eof()) return 0;
return res.getIntField("count");
}
std::string SQLiteDatabase::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 '%' || ? || '%' OR owner_id IN (SELECT id FROM charinfo WHERE name LIKE '%' || ? || '%') 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 ? OFFSET ?;";
// 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.eof() ? 0 : totalCountResult.getIntField("count");
// 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.eof() ? 0 : filteredCountResult.getIntField("count");
}
// Execute main query
auto [stmt, result] = !search.empty() ?
ExecuteSelect(mainQuery, search, search, searchId, searchId, length, start) :
ExecuteSelect(mainQuery, length, start);
// Build response JSON
nlohmann::json propertiesArray = nlohmann::json::array();
while (!result.eof()) {
nlohmann::json property = {
{"id", std::to_string(result.getInt64Field("id"))},
{"owner_id", std::to_string(result.getInt64Field("owner_id"))},
{"owner_name", result.fieldIsNull("owner_name") ? "" : result.getStringField("owner_name")},
{"name", result.getStringField("name")},
{"mod_approved", result.getIntField("mod_approved") != 0},
{"reputation", result.getInt64Field("reputation")},
{"performance_cost", result.fieldIsNull("performance_cost") ? 0.0 : result.getFloatField("performance_cost")},
{"models", result.getIntField("models")},
{"zone_id", result.getIntField("zone_id")}
};
propertiesArray.push_back(property);
result.nextRow();
}
nlohmann::json response = {
{"draw", 0},
{"recordsTotal", totalRecords},
{"recordsFiltered", filteredRecords},
{"data", propertiesArray}
};
return response.dump();
}
void SQLiteDatabase::ApproveProperty(const LWOOBJID propertyId) {
ExecuteUpdate("UPDATE properties SET mod_approved = 1 WHERE id = ?;", propertyId);
}
IProperty::ShowcaseResult SQLiteDatabase::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 '%' || ? || '%' ESCAPE '!' OR p.description LIKE '%' || ? || '%' ESCAPE '!' OR c.name LIKE '%' || ? || '%' 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 COLLATE NOCASE 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 [countStmt, count] = ExecuteSelect("SELECT COUNT(*) AS count" + from + ";", query.zoneId, query.zoneId, s, like, like, like);
showcase.total = count.eof() ? 0 : static_cast<uint32_t>(count.getIntField("count"));
auto [stmt, 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 ? OFFSET ?;", query.zoneId, query.zoneId, s, like, like, like, query.length, query.start);
for (; !result.eof(); result.nextRow()) {
showcase.entries.push_back({ ReadPropertyInfo(result), result.getStringField("owner_name"), static_cast<uint32_t>(result.getIntField("models")) });
}
return showcase;
}

View File

@@ -0,0 +1,106 @@
#include "SQLiteDatabase.h"
nlohmann::json SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"id", std::to_string(result.getInt64Field("id"))}, {"name", std::string(result.fieldIsNull("name") ? "" : result.getStringField("name"))}, {"zone_id", result.getIntField("zone_id")}, {"privacy_option", result.getIntField("privacy_option")},
{"mod_approved", result.getIntField("mod_approved") != 0}, {"rejection_reason", std::string(result.fieldIsNull("rejection_reason") ? "" : result.getStringField("rejection_reason"))}, {"last_updated", result.getInt64Field("last_updated")}, {"reputation", result.getInt64Field("reputation")}, {"models", result.getIntField("models")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"id", result.getIntField("id")}, {"body", std::string(result.fieldIsNull("body") ? "" : result.getStringField("body")).substr(0, 200)}, {"submitted", std::string(result.fieldIsNull("submitted") ? "" : result.getStringField("submitted"))}, {"resolved", !result.fieldIsNull("resolved_time")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"id", result.getInt64Field("id")}, {"created_at", result.getInt64Field("created_at")}, {"kind", result.getIntField("kind")}, {"details", std::string(result.fieldIsNull("details") ? "" : result.getStringField("details"))}, {"status", result.getIntField("status")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"id", std::to_string(result.getInt64Field("id"))}, {"name", std::string(result.fieldIsNull("name") ? "" : result.getStringField("name"))}, {"best", result.getIntField("best") != 0} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"id", result.getInt64Field("id")}, {"name", std::string(result.fieldIsNull("name") ? "" : result.getStringField("name"))}, {"message", std::string(result.fieldIsNull("violation_msg") ? "" : result.getStringField("violation_msg"))}, {"time", std::string(result.fieldIsNull("violation_time") ? "" : result.getStringField("violation_time"))} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"id", result.getInt64Field("id")}, {"actor", std::string(result.fieldIsNull("account_name") ? "" : result.getStringField("account_name"))}, {"action", std::string(result.fieldIsNull("action") ? "" : result.getStringField("action"))}, {"description", std::string(result.fieldIsNull("description") ? "" : result.getStringField("description"))}, {"time", result.getInt64Field("timestamp")} });
}
return rows;
}
void SQLiteDatabase::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 SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) {
rows.push_back({ {"subject", result.getStringField("subject")}, {"approved", result.getIntField("approved") != 0}, {"reason", result.getStringField("reason")}, {"time", result.getInt64Field("decided_at")} });
}
return rows;
}
nlohmann::json SQLiteDatabase::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.eof()) return nlohmann::json::object();
const auto text = [&result](const char* col) { return std::string(result.fieldIsNull(col) ? "" : result.getStringField(col)); };
const auto id = [&result](const char* col) -> nlohmann::json { return result.fieldIsNull(col) ? nlohmann::json() : nlohmann::json(std::to_string(result.getInt64Field(col))); };
return {
{"id", id("id")}, {"owner_id", id("owner_id")}, {"template_id", result.getIntField("template_id")}, {"clone_id", id("clone_id")},
{"name", text("name")}, {"description", text("description")}, {"rent_amount", result.getIntField("rent_amount")}, {"rent_due", result.getInt64Field("rent_due")},
{"privacy_option", result.getIntField("privacy_option")}, {"mod_approved", result.getIntField("mod_approved")}, {"last_updated", result.getInt64Field("last_updated")},
{"time_claimed", result.getInt64Field("time_claimed")}, {"rejection_reason", text("rejection_reason")}, {"reputation", result.getInt64Field("reputation")},
{"zone_id", result.getIntField("zone_id")}, {"performance_cost", result.fieldIsNull("performance_cost") ? 0.0 : result.getFloatField("performance_cost")}
};
}
nlohmann::json SQLiteDatabase::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);
for (; !result.eof(); result.nextRow()) {
const auto text = [&result](const char* col) -> nlohmann::json { return result.fieldIsNull(col) ? nlohmann::json() : nlohmann::json(std::string(result.getStringField(col))); };
const auto id = [&result](const char* col) -> nlohmann::json { return result.fieldIsNull(col) ? nlohmann::json() : nlohmann::json(std::to_string(result.getInt64Field(col))); };
const auto num = [&result](const char* col) -> nlohmann::json { return result.fieldIsNull(col) ? nlohmann::json() : nlohmann::json(result.getInt64Field(col)); };
rows.push_back({
{"id", id("id")}, {"property_id", id("property_id")}, {"ugc_id", id("ugc_id")}, {"lot", result.getIntField("lot")},
{"x", result.getFloatField("x")}, {"y", result.getFloatField("y")}, {"z", result.getFloatField("z")},
{"rx", result.getFloatField("rx")}, {"ry", result.getFloatField("ry")}, {"rz", result.getFloatField("rz")}, {"rw", result.getFloatField("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;
}

View File

@@ -0,0 +1,98 @@
#include "SQLiteDatabase.h"
#include <ctime>
namespace {
std::string Text(CppSQLite3Query& result, const char* field) {
return result.fieldIsNull(field) ? "" : result.getStringField(field);
}
IScheduledTasks::TaskRun RunRow(CppSQLite3Query& result, bool withLog) {
IScheduledTasks::TaskRun run;
run.id = static_cast<uint64_t>(result.getInt64Field("id"));
run.task = Text(result, "task");
run.trigger = Text(result, "task_trigger");
run.actor = Text(result, "actor");
run.startedAt = result.getInt64Field("started_at");
run.finishedAt = result.getInt64Field("finished_at");
run.status = static_cast<IScheduledTasks::eRunStatus>(result.getIntField("status"));
run.summary = Text(result, "summary");
if (withLog) run.log = Text(result, "log");
return run;
}
}
std::vector<IScheduledTasks::TaskSettings> SQLiteDatabase::GetScheduledTasks() {
std::vector<TaskSettings> tasks;
auto [_, result] = ExecuteSelect("SELECT * FROM scheduled_tasks ORDER BY name;");
while (!result.eof()) {
TaskSettings task;
task.name = Text(result, "name");
if (!result.fieldIsNull("schedule")) task.schedule = result.getStringField("schedule");
task.enabled = result.getIntField("enabled") != 0;
task.lastScheduledAt = result.getInt64Field("last_scheduled_at");
task.updatedAt = result.getInt64Field("updated_at");
task.updatedBy = Text(result, "updated_by");
tasks.push_back(std::move(task));
result.nextRow();
}
return tasks;
}
void SQLiteDatabase::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 CONFLICT(name) DO UPDATE SET schedule = excluded.schedule, enabled = excluded.enabled, updated_at = excluded.updated_at, updated_by = excluded.updated_by;",
name, schedule, enabled, static_cast<int64_t>(std::time(nullptr)), by);
}
void SQLiteDatabase::SetTaskLastScheduled(const std::string& name, int64_t time) {
ExecuteInsert("INSERT INTO scheduled_tasks (name, last_scheduled_at) VALUES (?, ?) ON CONFLICT(name) DO UPDATE SET last_scheduled_at = excluded.last_scheduled_at;",
name, time);
}
void SQLiteDatabase::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 SQLiteDatabase::GetTaskRunsTable(const std::string& task, uint32_t start, uint32_t length) {
const std::string where = task.empty() ? "" : " WHERE task = ?";
const auto count = [&]() {
auto [_, result] = task.empty() ? ExecuteSelect("SELECT COUNT(*) AS count FROM scheduled_task_runs;")
: ExecuteSelect("SELECT COUNT(*) AS count FROM scheduled_task_runs" + where + ";", task);
return result.eof() ? 0 : result.getIntField("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.eof()) {
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} });
result.nextRow();
}
return { {"draw", 0}, {"recordsTotal", count}, {"recordsFiltered", count}, {"data", data} };
}
std::optional<IScheduledTasks::TaskRun> SQLiteDatabase::GetTaskRun(uint64_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM scheduled_task_runs WHERE id = ?;", static_cast<int64_t>(id));
if (result.eof()) return std::nullopt;
return RunRow(result, true);
}
std::vector<IScheduledTasks::TaskRun> SQLiteDatabase::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.eof()) {
runs.push_back(RunRow(result, false));
result.nextRow();
}
return runs;
}
uint32_t SQLiteDatabase::PruneTaskRuns(int64_t beforeTime) {
return static_cast<uint32_t>(ExecuteUpdate("DELETE FROM scheduled_task_runs WHERE finished_at < ?;", beforeTime));
}

View File

@@ -0,0 +1,103 @@
#include "SQLiteDatabase.h"
#include <ctime>
void SQLiteDatabase::ReportConfigFromFile(const Setting& setting) {
ExecuteInsert(
"INSERT INTO server_config (file, name, file_value, file_source, secret, description, seen_at) VALUES (?, ?, ?, ?, ?, ?, ?) "
"ON CONFLICT(file, name) DO UPDATE SET file_value = excluded.file_value, file_source = excluded.file_source, secret = excluded.secret, "
"description = CASE WHEN excluded.description = '' THEN server_config.description ELSE excluded.description END, seen_at = excluded.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> SQLiteDatabase::GetServerConfig(const std::vector<std::string>& files) {
std::vector<Setting> settings;
auto [_, result] = ExecuteSelect("SELECT * FROM server_config ORDER BY file, name;");
while (!result.eof()) {
Setting setting;
setting.file = result.getStringField("file");
if (files.empty() || std::find(files.begin(), files.end(), setting.file) != files.end()) {
setting.name = result.getStringField("name");
if (!result.fieldIsNull("file_value")) setting.fileValue = result.getStringField("file_value");
setting.fileSource = result.getStringField("file_source");
if (!result.fieldIsNull("web_value")) setting.webValue = result.getStringField("web_value");
setting.webWins = result.getIntField("web_wins") != 0;
setting.secret = result.getIntField("secret") != 0;
setting.description = result.fieldIsNull("description") ? "" : result.getStringField("description");
setting.seenAt = result.getInt64Field("seen_at");
setting.updatedAt = result.getInt64Field("updated_at");
setting.updatedBy = result.getStringField("updated_by");
settings.push_back(std::move(setting));
}
result.nextRow();
}
return settings;
}
void SQLiteDatabase::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 CONFLICT(file, name) DO UPDATE SET web_value = excluded.web_value, web_wins = excluded.web_wins, updated_at = excluded.updated_at, updated_by = excluded.updated_by;",
file, name, value, webWins, static_cast<int64_t>(std::time(nullptr)), by);
}
void SQLiteDatabase::DeleteServerConfig(const std::string& file, const std::string& name) {
ExecuteDelete("DELETE FROM server_config WHERE file = ? AND name = ?;", file, name);
}
namespace {
std::optional<std::string> OptionalText(CppSQLite3Query& result, const char* field) {
if (result.fieldIsNull(field)) return std::nullopt;
return std::string(result.getStringField(field));
}
IServerConfig::SettingChange ReadSettingChange(CppSQLite3Query& result) {
IServerConfig::SettingChange change;
change.id = static_cast<uint64_t>(result.getInt64Field("id"));
change.file = result.getStringField("file");
change.name = result.getStringField("name");
change.oldValue = OptionalText(result, "old_value");
change.oldWebWins = result.getIntField("old_web_wins") != 0;
change.newValue = OptionalText(result, "new_value");
change.newWebWins = result.getIntField("new_web_wins") != 0;
change.fileValue = OptionalText(result, "file_value");
change.secret = result.getIntField("secret") != 0;
change.removed = result.getIntField("removed") != 0;
change.revertOf = static_cast<uint64_t>(result.getInt64Field("revert_of"));
change.changedAt = result.getInt64Field("changed_at");
change.accountId = static_cast<uint32_t>(result.getIntField("account_id"));
change.changedBy = result.getStringField("changed_by");
return change;
}
}
uint64_t SQLiteDatabase::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,
static_cast<int64_t>(change.revertOf), change.changedAt, change.accountId, change.changedBy);
auto [_, result] = ExecuteSelect("SELECT last_insert_rowid() AS id;");
return result.eof() ? 0 : static_cast<uint64_t>(result.getInt64Field("id"));
}
std::vector<IServerConfig::SettingChange> SQLiteDatabase::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.eof() ? 0 : static_cast<uint64_t>(count.getInt64Field("count"));
}
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);
for (; !result.eof(); result.nextRow()) changes.push_back(ReadSettingChange(result));
return changes;
}
std::optional<IServerConfig::SettingChange> SQLiteDatabase::GetSettingChange(uint64_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM server_config_history WHERE id = ?;", static_cast<int64_t>(id));
if (result.eof()) return std::nullopt;
return ReadSettingChange(result);
}

View File

@@ -0,0 +1,42 @@
#include "SQLiteDatabase.h"
void SQLiteDatabase::InsertHealthSample(const HealthSample& sample) {
ExecuteInsert("INSERT OR 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> SQLiteDatabase::GetHealthSamples(int64_t from, int64_t to, int64_t bucketSeconds) {
std::vector<HealthSample> samples;
auto [_, result] = ExecuteSelect("SELECT (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.eof()) {
samples.push_back({ result.getInt64Field("bucket"), static_cast<uint32_t>(result.getIntField("players")), static_cast<uint32_t>(result.getIntField("worlds")),
result.getIntField("auth") != 0, result.getIntField("chat") != 0, static_cast<uint64_t>(result.getFloatField("memory")) });
result.nextRow();
}
return samples;
}
uint32_t SQLiteDatabase::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 SQLiteDatabase::InsertInstanceSamples(const std::vector<InstanceSample>& samples) {
for (const auto& sample : samples) {
ExecuteInsert("INSERT OR 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> SQLiteDatabase::GetInstanceSamples(int64_t from, int64_t to, int64_t bucketSeconds, uint32_t zoneId) {
std::vector<InstanceSample> samples;
const std::string query = "SELECT (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);
for (; !result.eof(); result.nextRow()) {
samples.push_back({ result.getInt64Field("bucket"), static_cast<uint32_t>(result.getIntField("zone_id")), static_cast<uint32_t>(result.getIntField("instance_id")),
static_cast<uint32_t>(result.getIntField("clone_id")), static_cast<uint32_t>(result.getIntField("players")) });
}
return samples;
}

View File

@@ -0,0 +1,140 @@
#include "SQLiteDatabase.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;
}
IServerOperations::ScheduledAnnouncement ReadAnnouncement(CppSQLite3Query& result) {
IServerOperations::ScheduledAnnouncement row;
row.id = static_cast<uint64_t>(result.getInt64Field("id"));
row.title = result.getStringField("title");
row.message = result.getStringField("message");
row.zones = SplitZones(result.getStringField("zones"));
row.schedule = result.getStringField("schedule");
row.startsAt = result.getInt64Field("starts_at");
row.endsAt = result.getInt64Field("ends_at");
row.enabled = result.getIntField("enabled") != 0;
row.lastSentAt = result.getInt64Field("last_sent_at");
row.sentCount = static_cast<uint32_t>(result.getIntField("sent_count"));
row.createdAt = result.getInt64Field("created_at");
row.createdBy = result.getStringField("created_by");
row.updatedAt = result.getInt64Field("updated_at");
row.updatedBy = result.getStringField("updated_by");
return row;
}
IServerOperations::ScheduledEvent ReadEvent(CppSQLite3Query& result) {
IServerOperations::ScheduledEvent row;
row.id = static_cast<uint64_t>(result.getInt64Field("id"));
row.name = result.getStringField("name");
row.note = result.getStringField("note");
row.mode = static_cast<uint8_t>(result.getIntField("mode"));
row.schedule = result.getStringField("schedule");
row.startsAt = result.getInt64Field("starts_at");
row.endsAt = result.getInt64Field("ends_at");
row.priority = result.getIntField("priority");
row.parts = result.getStringField("parts");
row.state = static_cast<IServerOperations::eEventState>(result.getIntField("state"));
row.status = result.getStringField("status");
row.createdAt = result.getInt64Field("created_at");
row.createdBy = result.getStringField("created_by");
row.updatedAt = result.getInt64Field("updated_at");
row.updatedBy = result.getStringField("updated_by");
return row;
}
}
std::vector<IServerOperations::ScheduledAnnouncement> SQLiteDatabase::GetScheduledAnnouncements() {
std::vector<ScheduledAnnouncement> rows;
auto [_, result] = ExecuteSelect("SELECT * FROM scheduled_announcements ORDER BY id;");
for (; !result.eof(); result.nextRow()) rows.push_back(ReadAnnouncement(result));
return rows;
}
uint64_t SQLiteDatabase::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_rowid() AS id;");
return result.eof() ? 0 : static_cast<uint64_t>(result.getInt64Field("id"));
}
void SQLiteDatabase::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, static_cast<int64_t>(row.id));
}
void SQLiteDatabase::MarkAnnouncementSent(uint64_t id, int64_t time) {
ExecuteUpdate("UPDATE scheduled_announcements SET last_sent_at = ?, sent_count = sent_count + 1 WHERE id = ?;", time, static_cast<int64_t>(id));
}
void SQLiteDatabase::DeleteScheduledAnnouncement(uint64_t id) {
ExecuteDelete("DELETE FROM scheduled_announcements WHERE id = ?;", static_cast<int64_t>(id));
}
std::vector<IServerOperations::ScheduledEvent> SQLiteDatabase::GetScheduledEvents() {
std::vector<ScheduledEvent> rows;
auto [_, result] = ExecuteSelect("SELECT * FROM scheduled_events ORDER BY id;");
for (; !result.eof(); result.nextRow()) rows.push_back(ReadEvent(result));
return rows;
}
uint64_t SQLiteDatabase::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_rowid() AS id;");
return result.eof() ? 0 : static_cast<uint64_t>(result.getInt64Field("id"));
}
void SQLiteDatabase::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, static_cast<int64_t>(row.id));
}
void SQLiteDatabase::DeleteScheduledEvent(uint64_t id) {
ExecuteDelete("DELETE FROM scheduled_events WHERE id = ?;", static_cast<int64_t>(id));
}
std::vector<IServerOperations::ZoneLimit> SQLiteDatabase::GetZoneLimits() {
std::vector<ZoneLimit> rows;
auto [_, result] = ExecuteSelect("SELECT * FROM zone_limits ORDER BY zone_id;");
for (; !result.eof(); result.nextRow()) {
ZoneLimit row;
row.zoneId = static_cast<uint32_t>(result.getIntField("zone_id"));
if (!result.fieldIsNull("soft_cap")) row.softCap = static_cast<uint32_t>(result.getIntField("soft_cap"));
if (!result.fieldIsNull("hard_cap")) row.hardCap = static_cast<uint32_t>(result.getIntField("hard_cap"));
row.spareInstances = static_cast<uint32_t>(result.getIntField("spare_instances"));
row.updatedAt = result.getInt64Field("updated_at");
row.updatedBy = result.getStringField("updated_by");
rows.push_back(std::move(row));
}
return rows;
}
void SQLiteDatabase::SetZoneLimit(const ZoneLimit& row) {
ExecuteInsert("INSERT INTO zone_limits (zone_id, soft_cap, hard_cap, spare_instances, updated_at, updated_by) VALUES (?, ?, ?, ?, ?, ?) "
"ON CONFLICT(zone_id) DO UPDATE SET soft_cap = excluded.soft_cap, hard_cap = excluded.hard_cap, spare_instances = excluded.spare_instances, "
"updated_at = excluded.updated_at, updated_by = excluded.updated_by;",
row.zoneId, row.softCap, row.hardCap, row.spareInstances, row.updatedAt, row.updatedBy);
}
void SQLiteDatabase::DeleteZoneLimit(uint32_t zoneId) {
ExecuteDelete("DELETE FROM zone_limits WHERE zone_id = ?;", zoneId);
}

View File

@@ -0,0 +1,39 @@
#include "SQLiteDatabase.h"
std::vector<ISlashCommands::SlashCommand> SQLiteDatabase::GetSlashCommands() {
std::vector<SlashCommand> commands;
auto [_, result] = ExecuteSelect("SELECT * FROM slash_commands ORDER BY name;");
while (!result.eof()) {
SlashCommand command;
command.name = result.getStringField("name");
command.aliases = GeneralUtils::SplitString(result.getStringField("aliases"), ',');
command.help = result.getStringField("help");
command.info = result.getStringField("info");
command.defaultLevel = static_cast<uint8_t>(result.getIntField("default_level"));
command.minLevel = static_cast<uint8_t>(result.getIntField("min_level"));
command.fixed = result.getIntField("fixed") != 0;
command.clientHandled = result.getIntField("client_handled") != 0;
command.note = result.getStringField("note");
command.dashboardPermission = result.getStringField("dashboard_permission");
command.targetRule = result.getStringField("target_rule");
command.followsPermission = result.getIntField("follows_permission") != 0;
commands.push_back(std::move(command));
result.nextRow();
}
return commands;
}
void SQLiteDatabase::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 CONFLICT(name) DO UPDATE SET aliases = excluded.aliases, help = excluded.help, info = excluded.info, default_level = excluded.default_level, "
"min_level = excluded.min_level, fixed = excluded.fixed, client_handled = excluded.client_handled, note = excluded.note, dashboard_permission = excluded.dashboard_permission, target_rule = excluded.target_rule, follows_permission = excluded.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 SQLiteDatabase::DeleteSlashCommand(const std::string& name) {
ExecuteDelete("DELETE FROM slash_commands WHERE name = ?;", name);
}

View File

@@ -28,7 +28,7 @@ std::vector<IUgc::Model> SQLiteDatabase::GetUgcModels(const LWOOBJID& propertyId
}
std::vector<IUgc::Model> SQLiteDatabase::GetAllUgcModels() {
auto [_, result] = ExecuteSelect("SELECT u.id AS ugcID, pc.id AS modelID, lxfml FROM ugc AS u JOIN properties_contents AS pc ON pc.id = u.id;");
auto [_, result] = ExecuteSelect("SELECT u.id AS ugcID, pc.id AS modelID, lxfml FROM ugc AS u JOIN properties_contents AS pc ON pc.ugc_id = u.id WHERE pc.lot = 14 AND pc.ugc_id IS NOT NULL;");
std::vector<IUgc::Model> models;
while (!result.eof()) {
@@ -72,7 +72,8 @@ void SQLiteDatabase::UpdateUgcModelData(const LWOOBJID& modelId, std::stringstre
}
std::optional<IUgc::Model> SQLiteDatabase::GetUgcModel(const LWOOBJID ugcId) {
auto [_, result] = ExecuteSelect("SELECT u.id AS ugcID, pc.id AS modelID, lxfml FROM ugc AS u JOIN properties_contents AS pc ON pc.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, pc.id AS modelID, lxfml 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.eof()) {