mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
chore(dashboard): remove the Maintenance page
The page held one-off repairs from Nexus Dashboard (approve known pet names, find/delete orphaned pet names, remove every buff, fix property clone IDs, list mission rewards without a commendation price). Removed the page, its sidebar entry, its /api/maintenance/* routes, the `maintenance` permission and the two database calls only it used (GetAllPetNames, FixPropertyCloneIds). The scheduled tasks (lift expired bans, fill in pet owners, approve known pet names) and property model import/removal stay. Check: the Admin sidebar group has no Maintenance link; /maintenance is a 404; the Tasks page still lists "Approve known pet names" and "Fill in pet owners"; property import still works; the Permissions page no longer lists "Data maintenance". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,7 +102,6 @@ namespace {
|
||||
{ "events_manage", "Server", "Scheduled events: features", "Scheduled events that switch game features (event_1..event_8) on and off", 8 },
|
||||
{ "server_live_update", "Server", "Live updates", "Move every server and world instance onto a new build without a restart (players see a short loading screen), and cancel one", 9 },
|
||||
{ "instances_manage", "Server", "Instance limits", "Change players per instance and spare instances per zone (the master server applies them)", 9 },
|
||||
{ "maintenance", "Server", "Data maintenance", "Repair tools on the Maintenance page", 9 },
|
||||
{ "backups", "Server", "Database backups", "Make, download and delete database backups (downloads also need your password)", 9 },
|
||||
{ "webhooks", "Server", "Webhooks", "Add and change outgoing alert webhooks", 9 },
|
||||
{ "email_settings", "Server", "Email settings", "Connect the mail account and send test emails", 9 },
|
||||
|
||||
@@ -443,7 +443,6 @@ void RegisterDashboardRoutes() {
|
||||
SimplePage("/pet_names", Perm("moderate_pet_names"), "pet_names.jinja2", "pet_names", "Pet names");
|
||||
SimplePage("/send_mail", Perm("mail_send"), "send_mail.jinja2", "send_mail", "Send mail");
|
||||
SimplePage("/api_docs", 0, "api_docs.jinja2", "api_docs", "API documentation");
|
||||
SimplePage("/maintenance", Perm("maintenance"), "maintenance.jinja2", "maintenance", "Data maintenance tools");
|
||||
SimplePage("/webhooks", Perm("webhooks"), "webhooks.jinja2", "webhooks", "Outgoing webhooks for alerts");
|
||||
SimplePage("/tasks", Perm("tasks_view"), "tasks.jinja2", "tasks", "Scheduled tasks: schedules, runs and logs");
|
||||
Route(eHTTPMethod::GET, "/api/zones", Perm("characters_rescue"), "Every zone with its name, for pickers: {zones: [{id, name}]}", [](HTTPReply& reply, const HTTPContext&) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#include "MaintenanceRoutes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
#include "RouteUtils.h"
|
||||
@@ -10,10 +8,7 @@
|
||||
#include "WSRoutes.h"
|
||||
#include "Background.h"
|
||||
#include "Scheduler.h"
|
||||
#include "PlayerActions.h"
|
||||
#include "ClientAssets.h"
|
||||
#include "Database.h"
|
||||
#include "CDClientDatabase.h"
|
||||
#include "Game.h"
|
||||
#include "Logger.h"
|
||||
#include "eHTTPMethod.h"
|
||||
@@ -30,11 +25,6 @@ namespace {
|
||||
// Requests are capped at 3 MB by the web server, and JSON escaping makes the body bigger than the LXFML
|
||||
constexpr size_t MAX_IMPORT_BYTES = 2 * 1024 * 1024;
|
||||
|
||||
bool AnyoneOnline() {
|
||||
const auto state = ServerState::GetServerStateJson();
|
||||
return state["stats"].value("onlinePlayers", 0u) > 0;
|
||||
}
|
||||
|
||||
// Every pet in every inventory: pet id -> the character holding it
|
||||
std::map<LWOOBJID, LWOOBJID> PetHolders(GameDatabase& db, size_t& characters) {
|
||||
std::map<LWOOBJID, LWOOBJID> holders;
|
||||
@@ -103,132 +93,6 @@ void RegisterMaintenanceTasks() {
|
||||
}
|
||||
|
||||
void RegisterMaintenanceRoutes() {
|
||||
Route(eHTTPMethod::POST, "/api/maintenance/pet_names/auto_approve", Perm("maintenance"), "Approve pending pet names that were approved for another pet before",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const auto approved = Database::Get()->ApprovePreviouslyApprovedPetNames();
|
||||
Audit(context, "auto_approve_pet_names", std::to_string(approved) + " pet name(s)");
|
||||
if (approved) BroadcastTableChanged("pet_names");
|
||||
JsonSuccess(reply, { {"message", "Approved " + std::to_string(approved) + " pet name(s)"} });
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::POST, "/api/maintenance/pet_names/orphans", Perm("maintenance"),
|
||||
"Find (or with {delete: true}, remove) pet names whose pet no longer exists. Runs in the background: returns {requestId}",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const auto body = ParseBody(context);
|
||||
const bool remove = body && body->value("delete", false);
|
||||
const auto requestId = PlayerActions::Begin(context.accountId, std::chrono::minutes(30));
|
||||
const auto actor = context;
|
||||
const bool queued = Background::Run("pet_name_orphans", [remove](GameDatabase& db) -> nlohmann::json {
|
||||
size_t characters = 0;
|
||||
const auto holders = PetHolders(db, characters);
|
||||
|
||||
const auto names = db.GetAllPetNames();
|
||||
nlohmann::json orphans = nlohmann::json::array();
|
||||
for (const auto& [id, name] : names) {
|
||||
if (!holders.contains(id)) orphans.push_back({ {"id", std::to_string(id)}, {"name", name} });
|
||||
}
|
||||
nlohmann::json result{
|
||||
{"characters", characters}, {"petsFound", holders.size()}, {"petNames", names.size()},
|
||||
{"orphaned", orphans.size()}, {"sample", nlohmann::json(orphans.begin(), orphans.begin() + std::min<size_t>(orphans.size(), 20))}
|
||||
};
|
||||
if (!remove) return result;
|
||||
// A mismatch in how ids are stored would make every name look orphaned; never delete in that case
|
||||
if (!names.empty() && orphans.size() * 2 > names.size()) {
|
||||
result["refused"] = "Refusing to delete: more than half of all pet names look orphaned, which suggests an ID mismatch rather than real orphans";
|
||||
return result;
|
||||
}
|
||||
for (const auto& orphan : orphans) db.RejectPetName(std::stoll(orphan["id"].get<std::string>()));
|
||||
result["deleted"] = orphans.size();
|
||||
return result;
|
||||
}, [requestId, actor](nlohmann::json result, const std::string& error) {
|
||||
if (!error.empty()) return PlayerActions::Finish(requestId, { false, "The scan failed: " + error });
|
||||
if (result.contains("refused")) return PlayerActions::Finish(requestId, { false, result["refused"].get<std::string>(), result });
|
||||
if (result.contains("deleted")) {
|
||||
Audit(actor, "delete_orphaned_pet_names", std::to_string(result["deleted"].get<size_t>()) + " pet name(s)");
|
||||
BroadcastTableChanged("pet_names");
|
||||
}
|
||||
PlayerActions::Finish(requestId, { true, std::to_string(result["orphaned"].get<size_t>()) + " orphaned pet name(s)" +
|
||||
(result.contains("deleted") ? ", deleted" : ""), result });
|
||||
});
|
||||
if (!queued) {
|
||||
PlayerActions::Finish(requestId, { false, "This scan is already running" });
|
||||
return JsonError(reply, eHTTPStatusCode::CONFLICT, "This scan is already running");
|
||||
}
|
||||
JsonSuccess(reply, { {"requestId", requestId} });
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::POST, "/api/maintenance/remove_buffs", Perm("maintenance"),
|
||||
"Remove every buff from every character (only while nobody is online). Body: {dry_run}. Runs in the background: returns {requestId}",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const auto body = ParseBody(context);
|
||||
const bool dryRun = !body || body->value("dry_run", true);
|
||||
if (!dryRun && AnyoneOnline()) return JsonError(reply, eHTTPStatusCode::CONFLICT, "Players are online; their worlds would overwrite this. Try again when nobody is online.");
|
||||
|
||||
const auto requestId = PlayerActions::Begin(context.accountId, std::chrono::minutes(30));
|
||||
const auto actor = context;
|
||||
const bool queued = Background::Run("remove_buffs", [dryRun](GameDatabase& db) -> nlohmann::json {
|
||||
// First pass only records ids so no writes happen while the cursor is open
|
||||
std::vector<LWOOBJID> withBuffs;
|
||||
db.ForEachCharacterXml([&](LWOOBJID id, const std::string& xml) {
|
||||
if (xml.find("<buff") != std::string::npos) withBuffs.push_back(id);
|
||||
});
|
||||
if (dryRun) return { {"characters", withBuffs.size()}, {"dryRun", true} };
|
||||
|
||||
uint32_t changed = 0;
|
||||
for (const auto id : withBuffs) {
|
||||
tinyxml2::XMLDocument doc;
|
||||
const auto xml = db.GetCharacterXml(id);
|
||||
if (doc.Parse(xml.c_str()) != tinyxml2::XML_SUCCESS) continue;
|
||||
auto* dest = doc.FirstChildElement("obj") ? doc.FirstChildElement("obj")->FirstChildElement("dest") : nullptr;
|
||||
auto* buff = dest ? dest->FirstChildElement("buff") : nullptr;
|
||||
if (!buff) continue;
|
||||
dest->DeleteChild(buff);
|
||||
tinyxml2::XMLPrinter printer(nullptr, true);
|
||||
doc.Print(&printer);
|
||||
db.UpdateCharacterXml(id, printer.CStr());
|
||||
changed++;
|
||||
}
|
||||
return { {"characters", changed}, {"dryRun", false} };
|
||||
}, [requestId, actor](nlohmann::json result, const std::string& error) {
|
||||
if (!error.empty()) return PlayerActions::Finish(requestId, { false, "Failed: " + error });
|
||||
const auto count = result["characters"].get<size_t>();
|
||||
if (result["dryRun"].get<bool>()) return PlayerActions::Finish(requestId, { true, std::to_string(count) + " character(s) have buffs", result });
|
||||
Audit(actor, "remove_all_buffs", std::to_string(count) + " character(s)");
|
||||
PlayerActions::Finish(requestId, { true, "Removed buffs from " + std::to_string(count) + " character(s)", result });
|
||||
});
|
||||
if (!queued) {
|
||||
PlayerActions::Finish(requestId, { false, "This is already running" });
|
||||
return JsonError(reply, eHTTPStatusCode::CONFLICT, "This is already running");
|
||||
}
|
||||
JsonSuccess(reply, { {"requestId", requestId} });
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::POST, "/api/maintenance/fix_clone_ids", Perm("maintenance"), "Repair properties whose clone ID doesn't match their owner's",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const auto fixed = Database::Get()->FixPropertyCloneIds();
|
||||
Audit(context, "fix_property_clone_ids", std::to_string(fixed) + " propert(ies)");
|
||||
if (fixed) BroadcastTableChanged("properties");
|
||||
JsonSuccess(reply, { {"fixed", fixed}, {"message", "Fixed " + std::to_string(fixed) + " propert" + (fixed == 1 ? "y" : "ies")} });
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::GET, "/api/maintenance/missing_commendation_items", Perm("maintenance"), "Mission reward items with no commendation vendor price (game data check)",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
auto rows = CDClientDatabase::ExecuteQuery(
|
||||
"SELECT DISTINCT r.lot FROM ("
|
||||
"SELECT reward_item1 AS lot FROM Missions UNION SELECT reward_item2 FROM Missions "
|
||||
"UNION SELECT reward_item3 FROM Missions UNION SELECT reward_item4 FROM Missions) r "
|
||||
"LEFT JOIN ComponentsRegistry cr ON cr.id = r.lot AND cr.component_type = 11 "
|
||||
"LEFT JOIN ItemComponent ic ON ic.id = cr.component_id "
|
||||
"WHERE r.lot > 0 AND (ic.commendationLOT IS NULL OR ic.commendationCost IS NULL) ORDER BY r.lot;");
|
||||
nlohmann::json items = nlohmann::json::array();
|
||||
while (!rows.eof()) {
|
||||
const LOT lot = rows.getIntField("lot");
|
||||
items.push_back({ {"lot", lot}, {"name", ClientAssets::ItemName(lot)} });
|
||||
rows.nextRow();
|
||||
}
|
||||
JsonReply(reply, eHTTPStatusCode::OK, { {"items", items} });
|
||||
});
|
||||
|
||||
Route(eHTTPMethod::POST, "/api/properties/:id/import", Perm("properties_import"), "Import LXFML models onto a property. Body: {lxfml, position: [x,y,z] (optional)}",
|
||||
[](HTTPReply& reply, const HTTPContext& context) {
|
||||
const auto propertyId = PathId<LWOOBJID>(context.path, 2);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
// Operator data repairs (maintenance page) and model import
|
||||
// Property model import and removal
|
||||
void RegisterMaintenanceRoutes();
|
||||
|
||||
// Scheduled tasks (approving pet names that were approved before, filling in pet owners); before Scheduler::Initialize
|
||||
|
||||
@@ -118,8 +118,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% set adminPages = ["settings", "settings_history", "permissions", "tasks", "play_keys", "backups", "maintenance", "webhooks", "client_assets", "ugc"] %}
|
||||
{% if can.settings or can.permissions_manage or can.tasks_view or can.play_keys_manage or can.backups or can.maintenance or can.webhooks or can.client_files or can.ugc_manage %}
|
||||
{% set adminPages = ["settings", "settings_history", "permissions", "tasks", "play_keys", "backups", "webhooks", "client_assets", "ugc"] %}
|
||||
{% if can.settings or can.permissions_manage or can.tasks_view or can.play_keys_manage or can.backups or can.webhooks or can.client_files or can.ugc_manage %}
|
||||
<div class="nav-group">
|
||||
<button class="nav-group-toggle" type="button" data-bs-toggle="collapse" data-bs-target="#nav-admin" aria-expanded="{% if current_page in adminPages %}true{% else %}false{% endif %}" aria-controls="nav-admin">
|
||||
Server Admin<span class="nav-group-dot" title="Something in here needs attention"></span>
|
||||
@@ -131,7 +131,6 @@
|
||||
{% if can.tasks_view %}<a href="/tasks" class="list-group-item list-group-item-action{% if current_page == "tasks" %} active{% endif %}">Scheduled Tasks</a>{% endif %}
|
||||
{% if can.play_keys_manage %}<a href="/play_keys" class="list-group-item list-group-item-action{% if current_page == "play_keys" %} active{% endif %}">Play Keys</a>{% endif %}
|
||||
{% if can.backups %}<a href="/backups" class="list-group-item list-group-item-action{% if current_page == "backups" %} active{% endif %}">Backups</a>{% endif %}
|
||||
{% if can.maintenance %}<a href="/maintenance" class="list-group-item list-group-item-action{% if current_page == "maintenance" %} active{% endif %}">Maintenance</a>{% endif %}
|
||||
{% if can.webhooks %}<a href="/webhooks" class="list-group-item list-group-item-action{% if current_page == "webhooks" %} active{% endif %}">Webhooks</a>{% endif %}
|
||||
{% if can.ugc_manage %}<a href="/ugc" class="list-group-item list-group-item-action{% if current_page == "ugc" %} active{% endif %}">UGC Server</a>{% endif %}
|
||||
{% if can.client_files %}<a href="/client_assets" class="list-group-item list-group-item-action{% if current_page == "client_assets" %} active{% endif %}">Client Files</a>{% endif %}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
{% extends "base.jinja2" %}
|
||||
|
||||
{% block title %}Maintenance - DarkflameServer{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-1">Maintenance</h2>
|
||||
<p class="text-body-secondary mb-4">Data repairs. Every action is recorded in the audit log. Scans read every character, so they can pause the dashboard for a moment on large servers.</p>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><h5 class="mb-0">Pet names</h5></div>
|
||||
<div class="card-body">
|
||||
<p class="small">Pending names that were already approved for another pet are approved automatically every hour. Run it now:</p>
|
||||
<button class="btn btn-sm btn-outline-primary mb-3" data-action="petApprove">Approve previously approved names</button>
|
||||
<p class="small">Find pet names whose pet no longer exists on any character (e.g. the pet was released).</p>
|
||||
<button class="btn btn-sm btn-outline-primary" data-action="petScan">Scan</button>
|
||||
<button class="btn btn-sm btn-outline-danger d-none" data-action="petDelete" id="petDeleteBtn">Delete orphaned names</button>
|
||||
<pre class="small mt-2 mb-0 d-none" id="petResult"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><h5 class="mb-0">Buffs</h5></div>
|
||||
<div class="card-body">
|
||||
<p class="small">Remove every buff from every character, e.g. after a buff got stuck. Only possible while nobody is online.</p>
|
||||
<button class="btn btn-sm btn-outline-primary" data-action="buffScan">Count characters with buffs</button>
|
||||
<button class="btn btn-sm btn-outline-danger" data-action="buffRemove">Remove all buffs</button>
|
||||
<pre class="small mt-2 mb-0 d-none" id="buffResult"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><h5 class="mb-0">Property clone IDs</h5></div>
|
||||
<div class="card-body">
|
||||
<p class="small">Repair properties whose clone ID doesn't match their owner's (left behind by an old bug), which stops them loading.</p>
|
||||
<button class="btn btn-sm btn-outline-primary" data-action="fixClones">Fix clone IDs</button>
|
||||
<pre class="small mt-2 mb-0 d-none" id="cloneResult"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header"><h5 class="mb-0">Game data check</h5></div>
|
||||
<div class="card-body">
|
||||
<p class="small">Mission reward items that have no commendation vendor price in the game data.</p>
|
||||
<button class="btn btn-sm btn-outline-primary" data-action="commendation">Find items</button>
|
||||
<div class="small mt-2" id="commendationResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
function show(id, text) {
|
||||
var el = document.getElementById(id);
|
||||
el.textContent = text;
|
||||
el.classList.remove('d-none');
|
||||
}
|
||||
|
||||
var actions = {
|
||||
petApprove: function () { return api.action('/api/maintenance/pet_names/auto_approve', {}).then(function (d) { toast(d.message, 'success'); }); },
|
||||
petScan: function () {
|
||||
show('petResult', 'Scanning every character...');
|
||||
return api.job('/api/maintenance/pet_names/orphans', {}).then(function (d) {
|
||||
show('petResult', d.characters + ' characters scanned, ' + d.petsFound + ' pets found, ' + d.petNames + ' pet names stored.\n' +
|
||||
d.orphaned + ' orphaned' + (d.sample.length ? ', e.g.: ' + d.sample.map(function (p) { return p.name; }).join(', ') : ''));
|
||||
document.getElementById('petDeleteBtn').classList.toggle('d-none', !d.orphaned);
|
||||
});
|
||||
},
|
||||
petDelete: function () {
|
||||
if (!confirm('Delete the orphaned pet names?')) return Promise.resolve();
|
||||
return api.job('/api/maintenance/pet_names/orphans', { delete: true }).then(function (d) {
|
||||
show('petResult', 'Deleted ' + d.deleted + ' orphaned pet name(s).');
|
||||
document.getElementById('petDeleteBtn').classList.add('d-none');
|
||||
});
|
||||
},
|
||||
buffScan: function () {
|
||||
show('buffResult', 'Scanning every character...');
|
||||
return api.job('/api/maintenance/remove_buffs', { dry_run: true }).then(function (d) { show('buffResult', d.characters + ' character(s) have buffs'); });
|
||||
},
|
||||
buffRemove: function () {
|
||||
if (!confirm('Remove all buffs from every character?')) return Promise.resolve();
|
||||
return api.job('/api/maintenance/remove_buffs', { dry_run: false }).then(function (d) { show('buffResult', 'Removed buffs from ' + d.characters + ' character(s)'); });
|
||||
},
|
||||
fixClones: function () { return api.action('/api/maintenance/fix_clone_ids', {}).then(function (d) { show('cloneResult', d.message); }); },
|
||||
commendation: function () {
|
||||
return api.get('/api/maintenance/missing_commendation_items').then(function (d) {
|
||||
var el = document.getElementById('commendationResult');
|
||||
el.innerHTML = d.items.length
|
||||
? '<ul class="mb-0">' + d.items.map(function (i) { return '<li>' + esc(i.name) + ' <span class="text-body-secondary">(' + esc(i.lot) + ')</span></li>'; }).join('') + '</ul>'
|
||||
: 'Every mission reward item has a commendation price.';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest('[data-action]');
|
||||
if (!btn) return;
|
||||
btn.disabled = true;
|
||||
actions[btn.getAttribute('data-action')]().catch(function () {}).finally(function () { btn.disabled = false; });
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -9,23 +9,17 @@
|
||||
|
||||
#include "dCommonVars.h"
|
||||
|
||||
// Bulk data repairs run from the dashboard's maintenance page
|
||||
// Bulk data reads and repairs for dashboard reports and scheduled tasks
|
||||
class IDashboardMaintenance {
|
||||
public:
|
||||
// Approve pending pet names that an earlier pet with the same name was approved for. Returns rows changed.
|
||||
virtual uint32_t ApprovePreviouslyApprovedPetNames() = 0;
|
||||
|
||||
// Every pet name as (id, name)
|
||||
virtual std::vector<std::pair<LWOOBJID, std::string>> GetAllPetNames() = 0;
|
||||
|
||||
// Call visit for every character's XML, one row at a time
|
||||
virtual void ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) = 0;
|
||||
|
||||
// Same, but only characters whose XML contains `needle`; the database does the filtering
|
||||
virtual void ForEachCharacterXmlContaining(const std::string& needle, const std::function<void(LWOOBJID, const std::string&)>& visit) = 0;
|
||||
|
||||
// Set each property's clone id to its owner's prop_clone_id where they differ. Returns rows changed.
|
||||
virtual uint32_t FixPropertyCloneIds() = 0;
|
||||
};
|
||||
|
||||
#endif //!__IDASHBOARDMAINTENANCE__H__
|
||||
|
||||
@@ -333,10 +333,8 @@ public:
|
||||
nlohmann::json GetCloneOwners(const std::vector<uint32_t>& clones) override;
|
||||
nlohmann::json GetCloneVisitors(uint32_t zoneId, uint32_t cloneId, int64_t from, int64_t to, uint32_t limit) override;
|
||||
uint32_t ApprovePreviouslyApprovedPetNames() override;
|
||||
std::vector<std::pair<LWOOBJID, std::string>> GetAllPetNames() override;
|
||||
void ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) override;
|
||||
void ForEachCharacterXmlContaining(const std::string& needle, const std::function<void(LWOOBJID, const std::string&)>& visit) override;
|
||||
uint32_t FixPropertyCloneIds() override;
|
||||
std::optional<IAccountEmails::EmailInfo> GetAccountEmail(const uint32_t accountId) override;
|
||||
void SetAccountEmail(const uint32_t accountId, const std::string_view email, const bool confirmed) override;
|
||||
std::optional<uint32_t> GetAccountIdByConfirmedEmail(const std::string_view email) override;
|
||||
|
||||
@@ -339,13 +339,6 @@ uint32_t MySQLDatabase::ApprovePreviouslyApprovedPetNames() {
|
||||
"(SELECT name FROM (SELECT pet_name AS name FROM pet_names WHERE approved = 2) AS approved_names);"));
|
||||
}
|
||||
|
||||
std::vector<std::pair<LWOOBJID, std::string>> MySQLDatabase::GetAllPetNames() {
|
||||
std::vector<std::pair<LWOOBJID, std::string>> pets;
|
||||
auto result = ExecuteSelect("SELECT id, pet_name FROM pet_names;");
|
||||
while (result->next()) pets.emplace_back(result->getInt64("id"), std::string(result->getString("pet_name").c_str()));
|
||||
return pets;
|
||||
}
|
||||
|
||||
void MySQLDatabase::ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) {
|
||||
auto result = ExecuteSelect("SELECT id, xml_data FROM charxml;");
|
||||
while (result->next()) visit(result->getInt64("id"), std::string(result->getString("xml_data").c_str()));
|
||||
@@ -355,10 +348,3 @@ void MySQLDatabase::ForEachCharacterXmlContaining(const std::string& needle, con
|
||||
auto result = ExecuteSelect("SELECT id, xml_data FROM charxml WHERE LOCATE(?, xml_data) > 0;", needle);
|
||||
while (result->next()) visit(result->getInt64("id"), std::string(result->getString("xml_data").c_str()));
|
||||
}
|
||||
|
||||
uint32_t MySQLDatabase::FixPropertyCloneIds() {
|
||||
return static_cast<uint32_t>(ExecuteUpdate(
|
||||
"UPDATE properties SET clone_id = (SELECT c.prop_clone_id FROM charinfo c WHERE c.id = properties.owner_id) "
|
||||
"WHERE EXISTS (SELECT 1 FROM charinfo c WHERE c.id = properties.owner_id AND c.prop_clone_id IS NOT NULL "
|
||||
"AND (properties.clone_id IS NULL OR c.prop_clone_id != properties.clone_id));"));
|
||||
}
|
||||
|
||||
@@ -317,10 +317,8 @@ public:
|
||||
nlohmann::json GetCloneOwners(const std::vector<uint32_t>& clones) override;
|
||||
nlohmann::json GetCloneVisitors(uint32_t zoneId, uint32_t cloneId, int64_t from, int64_t to, uint32_t limit) override;
|
||||
uint32_t ApprovePreviouslyApprovedPetNames() override;
|
||||
std::vector<std::pair<LWOOBJID, std::string>> GetAllPetNames() override;
|
||||
void ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) override;
|
||||
void ForEachCharacterXmlContaining(const std::string& needle, const std::function<void(LWOOBJID, const std::string&)>& visit) override;
|
||||
uint32_t FixPropertyCloneIds() override;
|
||||
std::optional<IAccountEmails::EmailInfo> GetAccountEmail(const uint32_t accountId) override;
|
||||
void SetAccountEmail(const uint32_t accountId, const std::string_view email, const bool confirmed) override;
|
||||
std::optional<uint32_t> GetAccountIdByConfirmedEmail(const std::string_view email) override;
|
||||
|
||||
@@ -344,16 +344,6 @@ uint32_t SQLiteDatabase::ApprovePreviouslyApprovedPetNames() {
|
||||
"(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()) {
|
||||
@@ -369,10 +359,3 @@ void SQLiteDatabase::ForEachCharacterXmlContaining(const std::string& needle, co
|
||||
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));"));
|
||||
}
|
||||
|
||||
@@ -367,10 +367,8 @@ class TestSQLDatabase : public GameDatabase {
|
||||
nlohmann::json GetCloneOwners(const std::vector<uint32_t>& clones) override { return { {"owners", nlohmann::json::array()}, {"properties", nlohmann::json::array()} }; }
|
||||
nlohmann::json GetCloneVisitors(uint32_t zoneId, uint32_t cloneId, int64_t from, int64_t to, uint32_t limit) override { return nlohmann::json::array(); }
|
||||
uint32_t ApprovePreviouslyApprovedPetNames() override { return 0; };
|
||||
std::vector<std::pair<LWOOBJID, std::string>> GetAllPetNames() override { return {}; };
|
||||
void ForEachCharacterXml(const std::function<void(LWOOBJID, const std::string&)>& visit) override {};
|
||||
void ForEachCharacterXmlContaining(const std::string& needle, const std::function<void(LWOOBJID, const std::string&)>& visit) override {};
|
||||
uint32_t FixPropertyCloneIds() override { return 0; };
|
||||
std::optional<IAccountEmails::EmailInfo> GetAccountEmail(const uint32_t accountId) override { return std::nullopt; };
|
||||
void SetAccountEmail(const uint32_t accountId, const std::string_view email, const bool confirmed) override {};
|
||||
std::optional<uint32_t> GetAccountIdByConfirmedEmail(const std::string_view email) override { return std::nullopt; };
|
||||
|
||||
@@ -62,7 +62,7 @@ What people can do depends on their account's GM level. Out of the box:
|
||||
| 4+ | Ban and lock accounts; email password reset links |
|
||||
| 5+ | Moderate pet names, properties and leaderboards; revoke strikes; change a character's missions; change the chat filter's words; send and schedule announcements |
|
||||
| 8+ | Manage accounts (create, change email or password, reset two-factor login) and GM levels; edit characters and replace their XML; give items back; read whispers and team chat; send chat into the game; attach items to mail, to one player or everyone; import models; shut down worlds; schedule restarts and events; run economy checks; play keys, client files and the vanity files; see scheduled tasks, the audit log, server logs, crash dumps, server health and instance load; the developer tools |
|
||||
| 9 | Delete accounts; change scheduled tasks, instance limits, settings and permissions (these two are always GM 9 only), webhooks and email settings; backups; data maintenance |
|
||||
| 9 | Delete accounts; change scheduled tasks, instance limits, settings and permissions (these two are always GM 9 only), webhooks and email settings; backups |
|
||||
|
||||
Each of these is a named permission, and you can change the lowest GM level allowed for any of them without rebuilding
|
||||
or restarting:
|
||||
|
||||
@@ -626,7 +626,6 @@ TEST_F(ParitySeeded, PetNames) {
|
||||
Both("GetPetNamesTable order " + std::to_string(column), [&](GameDatabase& db) { return db.GetPetNamesTable(0, 10, "", column, false); });
|
||||
}
|
||||
Both("GetPetsWithUnknownOwner", [](GameDatabase& db) { return db.GetPetsWithUnknownOwner(); });
|
||||
Both("GetAllPetNames", [](GameDatabase& db) { auto names = db.GetAllPetNames(); std::sort(names.begin(), names.end()); return names; });
|
||||
Both("ApprovePreviouslyApprovedPetNames", [](GameDatabase& db) { return db.ApprovePreviouslyApprovedPetNames(); });
|
||||
Both("SetPetOwner", [](GameDatabase& db) { db.SetPetOwner(PET3, 0); return json{ db.GetPetsWithUnknownOwner(), db.GetPetNameInfo(PET3) }; });
|
||||
Both("ApprovePetName", [](GameDatabase& db) { db.ApprovePetName(PET3); return db.GetPetNameInfo(PET3); });
|
||||
@@ -708,7 +707,6 @@ TEST_F(ParitySeeded, Properties) {
|
||||
Both("GetPropertiesOwnedBy", [](GameDatabase& db) { return db.GetPropertiesOwnedBy(CHAR_ALICE); });
|
||||
Both("GetPropertyRecord", [](GameDatabase& db) { return json{ db.GetPropertyRecord(PROP1), db.GetPropertyRecord(1) }; });
|
||||
Both("GetPropertyModelRecords", [](GameDatabase& db) { return db.GetPropertyModelRecords(PROP1); });
|
||||
Both("FixPropertyCloneIds", [](GameDatabase& db) { return db.FixPropertyCloneIds(); });
|
||||
Both("GetDashboardSnapshot", [](GameDatabase& db) { return db.GetDashboardSnapshot(); });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user